import React, { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Box, Button, IconButton, Typography } from "@mui/material"; import useMediaQuery from "@mui/material/useMediaQuery"; import DashboardIcon from "@mui/icons-material/Dashboard"; import WorkOutlineIcon from "@mui/icons-material/WorkOutline"; import ViewKanbanIcon from "@mui/icons-material/ViewKanban"; import BusinessIcon from "@mui/icons-material/Business"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import SettingsIcon from "@mui/icons-material/Settings"; import AlarmIcon from "@mui/icons-material/Alarm"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import ShieldIcon from "@mui/icons-material/Shield"; import SearchIcon from "@mui/icons-material/Search"; import MemoryIcon from "@mui/icons-material/Memory"; import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import { Navigate, Route, Routes, useLocation, useNavigate, useParams, createBrowserRouter, RouterProvider } from "react-router-dom"; import { ToastProvider } from "./toast"; import { ConfirmProvider } from "./confirm"; import { PromptProvider } from "./prompt"; import JobTable from "./components/JobTable"; import NotificationsPopover from "./components/NotificationsPopover"; import type { JobTableColumns } from "./components/JobTable"; import { I18nProvider, useI18n } from "./i18n/I18nProvider"; import LoginPage from "./views/LoginPage"; import LandingPage from "./views/LandingPage"; import ForgotPasswordPage from "./views/ForgotPasswordPage"; import ResetPasswordPage from "./views/ResetPasswordPage"; import VerifyEmailPage from "./views/VerifyEmailPage"; import MicrosoftLegacyRelinkPage from "./views/MicrosoftLegacyRelinkPage"; import RouteErrorPage from "./views/RouteErrorPage"; import { api } from "./api"; import { resolveCaptureUrl } from "./captureUrl"; import { clearAuthClientState, setAuthUserKey } from "./auth"; import { AccountPlanProvider } from "./accountPlan"; import AppShell, { NavItem } from "./layout/AppShell"; import { getThemeModePref, setThemeModePref, subscribeToThemePreferenceChanges, ThemeModePref } from "./themePrefs"; import JobTrackerThemeProvider from "./ThemeProvider"; const AddJobModal = lazy(() => import("./components/AddJobModal")); const KanbanBoard = lazy(() => import("./components/KanbanBoard")); const DashboardView = lazy(() => import("./components/DashboardView")); const JobDiscoveryPage = lazy(() => import("./views/JobDiscoveryPage")); const CompaniesTable = lazy(() => import("./components/CompaniesTable")); const SettingsView = lazy(() => import("./components/SettingsView")); const RemindersView = lazy(() => import("./components/RemindersView")); const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog")); const ProfilePage = lazy(() => import("./views/ProfilePage")); const CareerWorkspacePage = lazy(() => import("./views/CareerWorkspacePage")); const CvBuilderPage = lazy(() => import("./views/CvBuilderPage")); const ApplicationWorkspacePage = lazy(() => import("./views/ApplicationWorkspacePage")); const CvBuilderEditor = lazy(() => import("./views/CvBuilderEditor")); const PublicCvPage = lazy(() => import("./views/PublicCvPage")); const ConnectedAccountsPage = lazy(() => import("./views/ConnectedAccountsPage")); const AdminAuditPage = lazy(() => import("./views/AdminAuditPage")); const AdminUsersPage = lazy(() => import("./views/AdminUsersPage")); const AdminSystemPage = lazy(() => import("./views/AdminSystemPage")); const CorrespondenceInboxPage = lazy(() => import("./views/CorrespondenceInboxPage")); const OperationsPage = lazy(() => import("./views/OperationsPage")); const NotFoundPage = lazy(() => import("./views/NotFoundPage")); type AuthConfig = { requireAuth: boolean }; type ThemeControls = { themeMode: ThemeModePref; onThemeModeChange: (value: ThemeModePref) => void }; const ThemeControlsContext = createContext(null); type MeResponse = { provider?: "local" | "google" | "external"; id?: string; email?: string; userName?: string; firstName?: string; lastName?: string; displayName?: string; avatarImageDataUrl?: string; roles?: string[]; plan?: "free" | "pro"; entitlements?: { ai?: boolean; proThemes?: boolean }; appVersion?: string; appCommitSha?: string; }; function breadcrumbsFor(path: string, t: (k: any) => string): string[] { if (path.startsWith("/dashboard")) return [t("home"), t("analytics"), t("overview")]; if (path.startsWith("/discover")) return [t("home"), "Discover jobs"]; if (/^\/jobs\/\d+/.test(path)) return [t("home"), t("jobApplications"), "Job details"]; if (path.startsWith("/jobs")) return [t("home"), t("jobApplications")]; if (path.startsWith("/reminders")) return [t("home"), t("reminders")]; if (path.startsWith("/operations")) return [t("home"), "Operations"]; if (path.startsWith("/kanban")) return [t("home"), t("kanbanBoard")]; if (path.startsWith("/companies")) return [t("home"), t("companies")]; if (path.startsWith("/correspondence/review")) return [t("home"), "Gmail review queue"]; if (path.startsWith("/correspondence")) return [t("home"), "Correspondence inbox"]; if (path.startsWith("/trash")) return [t("home"), t("trash")]; if (path.startsWith("/settings")) return [t("home"), t("settings")]; if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")]; if (path.startsWith("/career/builder")) return [t("home"), "Career Workspace", "CV Builder"]; if (path.startsWith("/career")) return [t("home"), "Career Workspace"]; if (path.startsWith("/settings/connected-accounts")) return [t("home"), t("settings"), "Connected accounts"]; if (path.startsWith("/admin/audit")) return [t("home"), t("admin"), t("auditLog")]; if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")]; if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")]; return [t("home")]; } function titleFor(path: string, t: (k: any) => string): string { if (path === "/dashboard") return t("dashboard"); if (path.startsWith("/reminders")) return t("reminders"); if (path.startsWith("/operations")) return "Operations"; if (path.startsWith("/discover")) return "Discover jobs"; if (/^\/jobs\/\d+/.test(path)) return "Job details"; if (path.startsWith("/jobs")) return t("jobApplications"); if (path.startsWith("/kanban")) return t("kanbanBoard"); if (path.startsWith("/companies")) return t("companies"); if (path.startsWith("/correspondence/review")) return "Gmail review queue"; if (path.startsWith("/correspondence")) return "Correspondence inbox"; if (path.startsWith("/trash")) return t("trash"); if (path.startsWith("/settings")) return t("settings"); if (path.startsWith("/profile")) return t("profile"); if (path.startsWith("/career/builder")) return "CV Builder"; if (path.startsWith("/career")) return "Career Workspace"; if (path.startsWith("/settings/connected-accounts")) return "Connected accounts"; if (path.startsWith("/admin/audit")) return t("auditLog"); if (path.startsWith("/admin/users")) return t("users"); if (path.startsWith("/admin/system")) return t("systemStatus"); return t("appTitle"); } function subtitleFor(path: string, t: (k: any) => string): string | undefined { if (path === "/dashboard") return t("dashboardPageSubtitle"); if (path.startsWith("/discover")) return "Search official job-board feeds and save opportunities to your tracker."; if (/^\/jobs\/\d+/.test(path)) return "Manage this application, its documents, timeline, and correspondence."; if (path.startsWith("/jobs")) return t("jobsPageSubtitle"); if (path.startsWith("/kanban")) return t("kanbanPageSubtitle"); if (path.startsWith("/reminders")) return t("remindersPageSubtitle"); if (path.startsWith("/correspondence/review")) return t("gmailReviewPageSubtitle"); if (path.startsWith("/correspondence")) return t("correspondencePageSubtitle"); return undefined; } function PageLoader() { return Loading...; } function LegacyApplicationRedirect() { const { id } = useParams(); const location = useLocation(); return ; } function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMode, onThemeModeChange }: { jobPageSize: 15 | 20 | 25; setJobPageSize: (n: 15 | 20 | 25) => void; jobColumns: JobTableColumns; setJobColumns: (c: JobTableColumns) => void; themeMode: ThemeModePref; onThemeModeChange: (v: ThemeModePref) => void; }) { const location = useLocation(); const navigate = useNavigate(); const { t } = useI18n(); const compactHeaderActions = useMediaQuery("(max-width:767.95px)"); const [addOpen, setAddOpen] = useState(false); const [captureUrl, setCaptureUrl] = useState(undefined); const [quickOpen, setQuickOpen] = useState(false); const [refreshToken, setRefreshToken] = useState(0); const [requireAuth, setRequireAuth] = useState(null); const [authResolved, setAuthResolved] = useState(false); const [isAdmin, setIsAdmin] = useState(false); const [me, setMe] = useState(null); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [reminderCount, setReminderCount] = useState(0); const [notificationCount, setNotificationCount] = useState(0); const [notificationAnchor, setNotificationAnchor] = useState(null); const path = location.pathname; const isJobs = path.startsWith("/jobs"); const shortcutHint = useMemo(() => ( typeof navigator !== "undefined" && /Mac|iPhone|iPod|iPad/.test(navigator.platform) ? "⌘K" : "Ctrl+K" ), []); useEffect(() => { api.get("/auth/config").then((r) => setRequireAuth(Boolean(r.data?.requireAuth))).catch(() => setRequireAuth(false)); }, []); // Quick-capture target: bookmarklet (/?add=) or PWA share (url in `add`, or a link // embedded in shared `addtext`). Opens Add Job pre-filled and strips the params. useEffect(() => { const url = resolveCaptureUrl(location.search); if (!url) return; setCaptureUrl(url); setAddOpen(true); const params = new URLSearchParams(location.search); params.delete("add"); params.delete("addtext"); navigate({ pathname: location.pathname, search: params.toString() }, { replace: true }); }, [location.search, location.pathname, navigate]); useEffect(() => { let active = true; api.get("/auth/me") .then((r) => { if (!active) return; setMe(r.data); setIsAdmin(Boolean(r.data?.roles?.includes("Admin"))); setAuthUserKey(r.data?.id || r.data?.email || r.data?.userName || null, false); }) .catch(() => { if (!active) return; setMe(null); setIsAdmin(false); clearAuthClientState(false); }) .finally(() => { if (active) setAuthResolved(true); }); return () => { active = false; }; }, []); useEffect(() => { const load = () => { api.get("/jobapplications/reminders", { params: { upcomingDays: 14 } }).then((r) => setReminderCount(Array.isArray(r.data) ? r.data.length : 0)).catch(() => setReminderCount(0)); }; load(); const id = window.setInterval(load, 60000); return () => window.clearInterval(id); }, []); useEffect(() => { const load = () => { api.get<{ count: number }>("/notifications/unread-count").then((r) => setNotificationCount(Math.max(0, Number(r.data?.count) || 0))).catch(() => setNotificationCount(0)); }; load(); const id = window.setInterval(load, 60000); window.addEventListener("notifications-changed", load); return () => { window.clearInterval(id); window.removeEventListener("notifications-changed", load); }; }, []); useEffect(() => { const onAuthChanged = () => { setAuthResolved(false); api.get("/auth/me") .then((r) => { setMe(r.data); setIsAdmin(Boolean(r.data?.roles?.includes("Admin"))); setAuthUserKey(r.data?.id || r.data?.email || r.data?.userName || null, false); }) .catch(() => { setMe(null); setIsAdmin(false); clearAuthClientState(false); }) .finally(() => setAuthResolved(true)); }; window.addEventListener("auth-changed", onAuthChanged); return () => window.removeEventListener("auth-changed", onAuthChanged); }, []); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setQuickOpen(true); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); if (requireAuth === null || !authResolved) return Loading...; if (requireAuth && !me) return ; const pageTitle = titleFor(path, t); const pageSubtitle = subtitleFor(path, t); const breadcrumbs = breadcrumbsFor(path, t); const setAndPersistPageSize = (n: 15 | 20 | 25) => { setJobPageSize(n); window.localStorage.setItem("jobPageSize", String(n)); }; const setAndPersistColumns = (next: JobTableColumns) => { setJobColumns(next); window.localStorage.setItem("jobColumns", JSON.stringify(next)); }; const fullName = [me?.firstName, me?.lastName].filter(Boolean).join(" "); const nav: NavItem[] = [ { to: "/dashboard", label: t("dashboard"), icon: , section: t("manage") }, { to: "/jobs", label: t("jobApplications"), icon: , section: t("manage") }, { to: "/discover", label: "Discover jobs", icon: , section: t("manage") }, { to: "/reminders", label: t("reminders"), icon: , badgeCount: reminderCount, section: t("manage") }, { to: "/kanban", label: t("kanbanBoard"), icon: , section: t("manage") }, { to: "/companies", label: t("companies"), icon: , section: t("manage") }, { to: "/career", label: "Career Workspace", icon: , section: t("manage") }, { to: "/career/builder", label: "CV Builder", icon: , section: t("manage") }, { to: "/trash", label: t("trash"), icon: , section: t("manage") }, ]; const navBottom: NavItem[] = [ { to: "/admin/audit", label: t("auditLog"), icon: , hidden: !isAdmin, section: t("admin") }, { to: "/admin/users", label: t("users"), icon: , hidden: !isAdmin, section: t("admin") }, { to: "/admin/system", label: t("system"), icon: , hidden: !isAdmin, section: t("admin") }, { to: "/profile", label: t("profile"), icon: , section: t("account") }, { to: "/settings", label: t("settings"), icon: , section: t("account") }, ]; const rightActions = ( {compactHeaderActions ? ( setQuickOpen(true)} sx={{ border: "1px solid", borderColor: "divider", borderRadius: 2.5, width: 42, height: 42, flex: "0 0 auto" }} > ) : ( )} {isJobs ? ( ) : null} ); return ( { setMobileDrawerOpen(false); navigate(to); }} user={{ email: me?.email, userName: me?.userName, displayName: me?.displayName || fullName || undefined, avatarImageDataUrl: me?.avatarImageDataUrl, roleLabel: isAdmin ? t("superAdmin") : t("user") }} buildMetadata={isAdmin && me?.appVersion ? { version: me.appVersion, commitSha: me.appCommitSha } : undefined} notificationsCount={notificationCount} onOpenNotifications={(anchor) => setNotificationAnchor(anchor)} onOpenSettings={() => navigate("/settings")} onOpenProfile={() => navigate("/profile")} onSignOut={() => { void api.post("/auth/logout").catch(() => undefined).finally(() => { clearAuthClientState(); navigate("/login"); }); }} rightActions={rightActions} > }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> setNotificationAnchor(null)} onNavigate={(to) => navigate(to)} onChanged={() => window.dispatchEvent(new Event("notifications-changed"))} /> { setAddOpen(false); setCaptureUrl(undefined); }} onCreated={() => { setRefreshToken((t) => t + 1); }} /> setQuickOpen(false)} onNavigate={(to) => navigate(to)} onOpenAddJob={() => setAddOpen(true)} /> ); } function ThemeAwareShell(props: Omit, "themeMode" | "onThemeModeChange">) { const controls = useContext(ThemeControlsContext); if (!controls) throw new Error("Theme controls are unavailable."); return ; } export default function App() { const systemPrefersDark = useMediaQuery("(prefers-color-scheme: dark)", { defaultMatches: true, noSsr: true }); const [themeMode, setThemeMode] = useState(() => getThemeModePref()); useEffect(() => { const sync = () => { setThemeMode(getThemeModePref()); }; return subscribeToThemePreferenceChanges(sync); }, []); const onThemeModeChange = useCallback((v: ThemeModePref) => { setThemeModePref(v); setThemeMode(v); }, []); const themeControls = useMemo(() => ({ themeMode, onThemeModeChange }), [onThemeModeChange, themeMode]); const [jobPageSize, setJobPageSize] = useState<15 | 20 | 25>(() => { const raw = window.localStorage.getItem("jobPageSize"); const n = raw ? Number(raw) : 15; return (n === 20 || n === 25 ? n : 15) as 15 | 20 | 25; }); const [jobColumns, setJobColumns] = useState(() => { const raw = window.localStorage.getItem("jobColumns"); if (!raw) return { status: true, dateApplied: true, daysSince: true, jobUrl: false }; try { const p = JSON.parse(raw) as Partial; return { status: p.status ?? true, dateApplied: p.dateApplied ?? true, daysSince: p.daysSince ?? true, jobUrl: p.jobUrl ?? false }; } catch { return { status: true, dateApplied: true, daysSince: true, jobUrl: false }; } }); const router = useMemo(() => createBrowserRouter([ { path: "/", element: , errorElement: }, { path: "/login", element: , errorElement: }, { path: "/register", element: , errorElement: }, { path: "/forgot-password", element: , errorElement: }, { path: "/reset-password", element: , errorElement: }, { path: "/verify-email", element: , errorElement: }, { path: "/confirm-email-change", element: , errorElement: }, { path: "/microsoft-legacy-relink", element: , errorElement: }, { path: "/cv/:slug", element: , errorElement: }, { path: "/*", element: , errorElement: }, ], { future: { v7_relativeSplatPath: true } }), [jobColumns, jobPageSize]); return ( ); }