diff --git a/orchestrator/src/client/App.tsx b/orchestrator/src/client/App.tsx index 04cea17..a336df2 100644 --- a/orchestrator/src/client/App.tsx +++ b/orchestrator/src/client/App.tsx @@ -3,7 +3,7 @@ */ import React, { useRef } from "react"; -import { Route, Routes, useLocation } from "react-router-dom"; +import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { CSSTransition, SwitchTransition } from "react-transition-group"; import { Toaster } from "@/components/ui/sonner"; @@ -16,11 +16,20 @@ export const App: React.FC = () => { const location = useLocation(); const nodeRef = useRef(null); + // Determine a stable key for transitions to avoid unnecessary unmounts when switching sub-tabs + const pageKey = React.useMemo(() => { + const firstSegment = location.pathname.split("/")[1] || "ready"; + if (["ready", "discovered", "applied", "all"].includes(firstSegment)) { + return "orchestrator"; + } + return firstSegment; + }, [location.pathname]); + return ( <> { >
- } /> + } /> } /> } /> } /> + } />
diff --git a/orchestrator/src/client/components/layout.tsx b/orchestrator/src/client/components/layout.tsx index 906ac8d..96a3640 100644 --- a/orchestrator/src/client/components/layout.tsx +++ b/orchestrator/src/client/components/layout.tsx @@ -81,7 +81,7 @@ export const PageHeader: React.FC = ({ onClick={() => handleNavClick(to)} className={cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground text-left", - location.pathname === to + location.pathname === to || (to === "/" && ["/ready", "/discovered", "/applied", "/all"].includes(location.pathname)) ? "bg-accent text-accent-foreground" : "text-muted-foreground" )} diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 94d528a..3249589 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -3,6 +3,7 @@ */ import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -24,18 +25,82 @@ import { usePipelineSources } from "./orchestrator/usePipelineSources"; import { getJobCounts } from "./orchestrator/utils"; export const OrchestratorPage: React.FC = () => { + const { tab } = useParams<{ tab: string }>(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const activeTab = useMemo(() => { + const validTabs: FilterTab[] = ["ready", "discovered", "applied", "all"]; + if (tab && validTabs.includes(tab as FilterTab)) { + return tab as FilterTab; + } + return "ready"; + }, [tab]); + + // Sync searchQuery with URL + const searchQuery = searchParams.get("q") || ""; + const setSearchQuery = (q: string) => { + setSearchParams( + (prev) => { + if (q) prev.set("q", q); + else prev.delete("q"); + return prev; + }, + { replace: true }, + ); + }; + + // Sync sourceFilter with URL + const sourceFilter = (searchParams.get("source") as JobSource | "all") || "all"; + const setSourceFilter = (source: JobSource | "all") => { + setSearchParams( + (prev) => { + if (source !== "all") prev.set("source", source); + else prev.delete("source"); + return prev; + }, + { replace: true }, + ); + }; + + // Sync sort with URL + const sort = useMemo((): JobSort => { + const s = searchParams.get("sort"); + if (!s) return DEFAULT_SORT; + const [key, direction] = s.split(":"); + return { key: key as any, direction: direction as any }; + }, [searchParams]); + + const setSort = (newSort: JobSort) => { + setSearchParams( + (prev) => { + prev.set("sort", `${newSort.key}:${newSort.direction}`); + return prev; + }, + { replace: true }, + ); + }; + + // Effect to sync URL if it was invalid + useEffect(() => { + const validTabs: FilterTab[] = ["ready", "discovered", "applied", "all"]; + if (tab && !validTabs.includes(tab as FilterTab)) { + navigate("/ready", { replace: true }); + } + }, [tab, navigate]); + const [navOpen, setNavOpen] = useState(false); const [isManualImportOpen, setIsManualImportOpen] = useState(false); - const [activeTab, setActiveTab] = useState("ready"); - const [searchQuery, setSearchQuery] = useState(""); - const [sourceFilter, setSourceFilter] = useState("all"); - const [sort, setSort] = useState(DEFAULT_SORT); const [selectedJobId, setSelectedJobId] = useState(null); const [isDetailDrawerOpen, setIsDetailDrawerOpen] = useState(false); const [isDesktop, setIsDesktop] = useState( () => (typeof window !== "undefined" ? window.matchMedia("(min-width: 1024px)").matches : false), ); + const setActiveTab = (newTab: FilterTab) => { + navigate(`/${newTab}`); + }; + const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(); const { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData(); diff --git a/orchestrator/src/client/pages/UkVisaJobsPage.tsx b/orchestrator/src/client/pages/UkVisaJobsPage.tsx index 07ee550..b2d3d51 100644 --- a/orchestrator/src/client/pages/UkVisaJobsPage.tsx +++ b/orchestrator/src/client/pages/UkVisaJobsPage.tsx @@ -344,7 +344,7 @@ export const UkVisaJobsPage: React.FC = () => { }} className={cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground text-left", - location.pathname === to + location.pathname === to || (to === "/" && ["/ready", "/discovered", "/applied", "/all"].includes(location.pathname)) ? "bg-accent text-accent-foreground" : "text-muted-foreground" )} diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx index 68f339c..a620965 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx @@ -95,7 +95,7 @@ export const OrchestratorHeader: React.FC = ({ setTimeout(() => navigate(to), 150); }} className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground text-left ${ - location.pathname === to + location.pathname === to || (to === "/" && ["/ready", "/discovered", "/applied", "/all"].includes(location.pathname)) ? "bg-accent text-accent-foreground" : "text-muted-foreground" }`}