try.jobops.app
This commit is contained in:
parent
11d1e9820b
commit
daca4d2bd4
26
README.md
26
README.md
@ -5,9 +5,12 @@
|
||||
[](https://github.com/DaKheera47/job-ops/pkgs/container/job-ops)
|
||||
[](https://github.com/DaKheera47/job-ops/actions/workflows/ghcr.yml)
|
||||
[](Contributors)
|
||||
[](https://try.jobops.app?utm_source=github&utm_medium=badge&utm_campaign=waitlist)
|
||||
|
||||
<img width="1200" height="600" alt="Jobops-banner-900" src="https://github.com/user-attachments/assets/e929e389-2ebb-4de1-82c6-8e136b849b78" />
|
||||
|
||||
Stop applying blind.
|
||||
|
||||
Scrapes major job boards (LinkedIn, Indeed, Glassdoor & more), AI-scores suitability, tailors resumes (RxResume), and tracks application emails automatically.
|
||||
|
||||
You still apply to every job yourself. JobOps just finds jobs, makes sure you're applying to the right ones with a tailored CV, and not losing track of where you're at.
|
||||
@ -46,12 +49,6 @@ If you want the serious view of the project, start here:
|
||||
- [Extractor System](https://jobops.dakheera47.com/docs/extractors/overview)
|
||||
- [Troubleshooting](https://jobops.dakheera47.com/docs/troubleshooting/common-problems)
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to contribute code, docs, or extractors? Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||
|
||||
That guide is intentionally link-first so contributor workflow lives in one place while setup and feature docs stay in the canonical docs site.
|
||||
|
||||
## Quick Start (10 Min)
|
||||
|
||||
Prefer guided setup? Follow the [Self-Hosting Guide](https://jobops.dakheera47.com/docs/getting-started/self-hosting).
|
||||
@ -110,6 +107,23 @@ See [post-application tracking docs](https://jobops.dakheera47.com/docs/features
|
||||
|
||||
**Note on Analytics**: The alpha version includes anonymous analytics (Umami) to help debug performance. To opt-out, block `umami.dakheera47.com` in your firewall/DNS.
|
||||
|
||||
## ☁️ Cloud Version (Coming Soon)
|
||||
|
||||
Self-hosting not your thing? A hosted version of JobOps is coming.
|
||||
|
||||
- No Docker required
|
||||
- Up and running in 2 minutes
|
||||
- Managed updates
|
||||
- Self-hosted will always be free and open source
|
||||
|
||||
👉 Join the waitlist at https://try.jobops.app?utm_source=github&utm_medium=readme&utm_campaign=waitlist
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to contribute code, docs, or extractors? Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||
|
||||
That guide is intentionally link-first so contributor workflow lives in one place while setup and feature docs stay in the canonical docs site.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#DaKheera47/job-ops&type=date&legend=top-left">
|
||||
|
||||
@ -2,7 +2,6 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { trackEvent } from "@/lib/analytics";
|
||||
import { App } from "./App";
|
||||
import { useDemoInfo } from "./hooks/useDemoInfo";
|
||||
|
||||
@ -10,10 +9,6 @@ vi.mock("./hooks/useDemoInfo", () => ({
|
||||
useDemoInfo: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/analytics", () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react-transition-group", () => ({
|
||||
SwitchTransition: ({ children }: { children: React.ReactNode }) => children,
|
||||
CSSTransition: ({ children }: { children: React.ReactNode }) => children,
|
||||
@ -66,9 +61,10 @@ vi.mock("./pages/VisaSponsorsPage", () => ({
|
||||
describe("App demo banner", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("shows a Star repo link in demo mode and tracks click", () => {
|
||||
it("shows a waitlist link in demo mode", () => {
|
||||
vi.mocked(useDemoInfo).mockReturnValue({
|
||||
demoMode: true,
|
||||
resetCadenceHours: 6,
|
||||
@ -84,18 +80,14 @@ describe("App demo banner", () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link", { name: /star .*repo/i });
|
||||
const link = screen.getByRole("link", { name: "try.jobops.app" });
|
||||
expect(link).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/DaKheera47/job-ops",
|
||||
"https://try.jobops.app?utm_source=demo&utm_medium=banner&utm_campaign=waitlist",
|
||||
);
|
||||
fireEvent.click(link);
|
||||
expect(trackEvent).toHaveBeenCalledWith("star_repo_click", {
|
||||
location: "demo_mode_banner",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not render the demo banner CTA when demo mode is disabled", () => {
|
||||
it("does not render the demo banner waitlist link when demo mode is disabled", () => {
|
||||
vi.mocked(useDemoInfo).mockReturnValue({
|
||||
demoMode: false,
|
||||
resetCadenceHours: 6,
|
||||
@ -111,6 +103,32 @@ describe("App demo banner", () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("link", { name: /star .*repo/i })).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: "try.jobops.app" })).toBeNull();
|
||||
});
|
||||
|
||||
it("lets the user dismiss the waitlist banner and keeps it hidden", () => {
|
||||
vi.mocked(useDemoInfo).mockReturnValue({
|
||||
demoMode: true,
|
||||
resetCadenceHours: 6,
|
||||
lastResetAt: null,
|
||||
nextResetAt: null,
|
||||
baselineVersion: null,
|
||||
baselineName: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/overview"]}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: /dismiss demo waitlist banner/i }),
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("link", { name: "try.jobops.app" })).toBeNull();
|
||||
expect(localStorage.getItem("jobops.demoWaitlistBannerDismissed")).toBe(
|
||||
"1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,12 +2,13 @@
|
||||
* Main App component.
|
||||
*/
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { trackEvent } from "@/lib/analytics";
|
||||
import { BasicAuthPrompt } from "./components/BasicAuthPrompt";
|
||||
import { OnboardingGate } from "./components/OnboardingGate";
|
||||
import { useDemoInfo } from "./hooks/useDemoInfo";
|
||||
@ -39,10 +40,20 @@ const REDIRECTS: Array<{ from: string; to: string }> = [
|
||||
{ from: "/all/:jobId", to: "/jobs/all/:jobId" },
|
||||
];
|
||||
|
||||
const DEMO_WAITLIST_BANNER_DISMISSED_KEY = "jobops.demoWaitlistBannerDismissed";
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const demoInfo = useDemoInfo();
|
||||
const [demoWaitlistBannerDismissed, setDemoWaitlistBannerDismissed] =
|
||||
useState(() => {
|
||||
try {
|
||||
return localStorage.getItem(DEMO_WAITLIST_BANNER_DISMISSED_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Determine a stable key for transitions to avoid unnecessary unmounts when switching sub-tabs
|
||||
const pageKey = React.useMemo(() => {
|
||||
@ -57,22 +68,45 @@ export const App: React.FC = () => {
|
||||
<>
|
||||
<OnboardingGate />
|
||||
<BasicAuthPrompt />
|
||||
{demoInfo?.demoMode && !demoWaitlistBannerDismissed && (
|
||||
<div className="sticky top-0 z-50 w-full border-b border-orange-400/60 bg-orange-500 px-4 py-2 text-xs text-orange-950 shadow-sm">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-center gap-3">
|
||||
<p className="flex-1 text-center font-medium">
|
||||
This is a read-only demo. Want JobOps without the Docker setup? ☁️{" "}
|
||||
Cloud version coming soon — join the waitlist at{" "}
|
||||
<a
|
||||
className="font-semibold underline underline-offset-2 hover:text-orange-900"
|
||||
href="https://try.jobops.app?utm_source=demo&utm_medium=banner&utm_campaign=waitlist"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
try.jobops.app
|
||||
</a>
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0 rounded-full text-orange-950 hover:bg-orange-400/30 hover:text-orange-950"
|
||||
onClick={() => {
|
||||
setDemoWaitlistBannerDismissed(true);
|
||||
try {
|
||||
localStorage.setItem(DEMO_WAITLIST_BANNER_DISMISSED_KEY, "1");
|
||||
} catch {
|
||||
// Ignore storage errors in restricted browser contexts.
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Dismiss demo waitlist banner</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{demoInfo?.demoMode && (
|
||||
<div className="w-full border-b border-amber-400/50 bg-amber-500/20 px-4 py-2 text-center text-xs text-amber-100 backdrop-blur">
|
||||
Demo mode: integrations are simulated and data resets every{" "}
|
||||
{demoInfo.resetCadenceHours} hours.{" "}
|
||||
<a
|
||||
className="font-semibold underline underline-offset-2 hover:text-amber-50"
|
||||
href="https://github.com/DaKheera47/job-ops"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() =>
|
||||
trackEvent("star_repo_click", { location: "demo_mode_banner" })
|
||||
}
|
||||
>
|
||||
Star the repo on GitHub
|
||||
</a>
|
||||
.
|
||||
{demoInfo.resetCadenceHours} hours.
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user