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/pkgs/container/job-ops)
|
||||||
[](https://github.com/DaKheera47/job-ops/actions/workflows/ghcr.yml)
|
[](https://github.com/DaKheera47/job-ops/actions/workflows/ghcr.yml)
|
||||||
[](Contributors)
|
[](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" />
|
<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.
|
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.
|
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)
|
- [Extractor System](https://jobops.dakheera47.com/docs/extractors/overview)
|
||||||
- [Troubleshooting](https://jobops.dakheera47.com/docs/troubleshooting/common-problems)
|
- [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)
|
## Quick Start (10 Min)
|
||||||
|
|
||||||
Prefer guided setup? Follow the [Self-Hosting Guide](https://jobops.dakheera47.com/docs/getting-started/self-hosting).
|
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.
|
**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
|
## Star History
|
||||||
|
|
||||||
<a href="https://www.star-history.com/#DaKheera47/job-ops&type=date&legend=top-left">
|
<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 type React from "react";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { trackEvent } from "@/lib/analytics";
|
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { useDemoInfo } from "./hooks/useDemoInfo";
|
import { useDemoInfo } from "./hooks/useDemoInfo";
|
||||||
|
|
||||||
@ -10,10 +9,6 @@ vi.mock("./hooks/useDemoInfo", () => ({
|
|||||||
useDemoInfo: vi.fn(),
|
useDemoInfo: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/analytics", () => ({
|
|
||||||
trackEvent: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("react-transition-group", () => ({
|
vi.mock("react-transition-group", () => ({
|
||||||
SwitchTransition: ({ children }: { children: React.ReactNode }) => children,
|
SwitchTransition: ({ children }: { children: React.ReactNode }) => children,
|
||||||
CSSTransition: ({ children }: { children: React.ReactNode }) => children,
|
CSSTransition: ({ children }: { children: React.ReactNode }) => children,
|
||||||
@ -66,9 +61,10 @@ vi.mock("./pages/VisaSponsorsPage", () => ({
|
|||||||
describe("App demo banner", () => {
|
describe("App demo banner", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
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({
|
vi.mocked(useDemoInfo).mockReturnValue({
|
||||||
demoMode: true,
|
demoMode: true,
|
||||||
resetCadenceHours: 6,
|
resetCadenceHours: 6,
|
||||||
@ -84,18 +80,14 @@ describe("App demo banner", () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const link = screen.getByRole("link", { name: /star .*repo/i });
|
const link = screen.getByRole("link", { name: "try.jobops.app" });
|
||||||
expect(link).toHaveAttribute(
|
expect(link).toHaveAttribute(
|
||||||
"href",
|
"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({
|
vi.mocked(useDemoInfo).mockReturnValue({
|
||||||
demoMode: false,
|
demoMode: false,
|
||||||
resetCadenceHours: 6,
|
resetCadenceHours: 6,
|
||||||
@ -111,6 +103,32 @@ describe("App demo banner", () => {
|
|||||||
</MemoryRouter>,
|
</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.
|
* 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 { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { trackEvent } from "@/lib/analytics";
|
|
||||||
import { BasicAuthPrompt } from "./components/BasicAuthPrompt";
|
import { BasicAuthPrompt } from "./components/BasicAuthPrompt";
|
||||||
import { OnboardingGate } from "./components/OnboardingGate";
|
import { OnboardingGate } from "./components/OnboardingGate";
|
||||||
import { useDemoInfo } from "./hooks/useDemoInfo";
|
import { useDemoInfo } from "./hooks/useDemoInfo";
|
||||||
@ -39,10 +40,20 @@ const REDIRECTS: Array<{ from: string; to: string }> = [
|
|||||||
{ from: "/all/:jobId", to: "/jobs/all/:jobId" },
|
{ from: "/all/:jobId", to: "/jobs/all/:jobId" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEMO_WAITLIST_BANNER_DISMISSED_KEY = "jobops.demoWaitlistBannerDismissed";
|
||||||
|
|
||||||
export const App: React.FC = () => {
|
export const App: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
const demoInfo = useDemoInfo();
|
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
|
// Determine a stable key for transitions to avoid unnecessary unmounts when switching sub-tabs
|
||||||
const pageKey = React.useMemo(() => {
|
const pageKey = React.useMemo(() => {
|
||||||
@ -57,22 +68,45 @@ export const App: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<OnboardingGate />
|
<OnboardingGate />
|
||||||
<BasicAuthPrompt />
|
<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 && (
|
{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">
|
<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{" "}
|
Demo mode: integrations are simulated and data resets every{" "}
|
||||||
{demoInfo.resetCadenceHours} hours.{" "}
|
{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>
|
|
||||||
.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user