diff --git a/README.md b/README.md index 484988b..18ab5de 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,12 @@ [![GHCR](https://img.shields.io/badge/docker-ghcr.io-blue?logo=docker&logoColor=white)](https://github.com/DaKheera47/job-ops/pkgs/container/job-ops) [![Release](https://github.com/DaKheera47/job-ops/actions/workflows/ghcr.yml/badge.svg)](https://github.com/DaKheera47/job-ops/actions/workflows/ghcr.yml) [![Contributors](https://img.shields.io/github/contributors-anon/dakheera47/job-ops)](Contributors) +[![Cloud Waitlist](https://img.shields.io/badge/☁️_Cloud-Join_Waitlist-orange?style=flat-square)](https://try.jobops.app?utm_source=github&utm_medium=badge&utm_campaign=waitlist) Jobops-banner-900 +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 diff --git a/orchestrator/src/client/App.test.tsx b/orchestrator/src/client/App.test.tsx index 9d2c8d7..682891d 100644 --- a/orchestrator/src/client/App.test.tsx +++ b/orchestrator/src/client/App.test.tsx @@ -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", () => { , ); - 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", () => { , ); - 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( + + + , + ); + + 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", + ); }); }); diff --git a/orchestrator/src/client/App.tsx b/orchestrator/src/client/App.tsx index c1b79a7..a2f22c5 100644 --- a/orchestrator/src/client/App.tsx +++ b/orchestrator/src/client/App.tsx @@ -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(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 = () => { <> + {demoInfo?.demoMode && !demoWaitlistBannerDismissed && ( +
+
+

+ This is a read-only demo. Want JobOps without the Docker setup? ☁️{" "} + Cloud version coming soon — join the waitlist at{" "} + + try.jobops.app + +

+ +
+
+ )} {demoInfo?.demoMode && (
Demo mode: integrations are simulated and data resets every{" "} - {demoInfo.resetCadenceHours} hours.{" "} - - trackEvent("star_repo_click", { location: "demo_mode_banner" }) - } - > - Star the repo on GitHub - - . + {demoInfo.resetCadenceHours} hours.
)}