import { createRootRoute, type ErrorComponentProps, Outlet, useLocation, useMatchRoute, useNavigate, useRouter, } from "@tanstack/react-router"; import { useState } from "react"; import "../app.css"; import { CandidateForm } from "../components/CandidateForm"; import { ConfirmDialog } from "../components/ConfirmDialog"; import { ExternalLinkDialog } from "../components/ExternalLinkDialog"; import { ItemForm } from "../components/ItemForm"; import { OnboardingWizard } from "../components/OnboardingWizard"; import { SlideOutPanel } from "../components/SlideOutPanel"; import { TotalsBar } from "../components/TotalsBar"; import { useAuth } from "../hooks/useAuth"; import { useDeleteCandidate } from "../hooks/useCandidates"; import { useOnboardingComplete } from "../hooks/useSettings"; import { useResolveThread, useThread } from "../hooks/useThreads"; import { useUIStore } from "../stores/uiStore"; export const Route = createRootRoute({ component: RootLayout, errorComponent: RootErrorBoundary, }); function RootErrorBoundary({ error, reset }: ErrorComponentProps) { const router = useRouter(); return (

Something went wrong

{error instanceof Error ? error.message : "An unexpected error occurred"}

); } function RootLayout() { const navigate = useNavigate(); const location = useLocation(); const { data: auth, isLoading: authLoading } = useAuth(); const isAuthenticated = !!auth?.user; // Item panel state const panelMode = useUIStore((s) => s.panelMode); const editingItemId = useUIStore((s) => s.editingItemId); const openAddPanel = useUIStore((s) => s.openAddPanel); const closePanel = useUIStore((s) => s.closePanel); // Candidate panel state const candidatePanelMode = useUIStore((s) => s.candidatePanelMode); const editingCandidateId = useUIStore((s) => s.editingCandidateId); const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel); // Candidate delete state const confirmDeleteCandidateId = useUIStore( (s) => s.confirmDeleteCandidateId, ); const closeConfirmDeleteCandidate = useUIStore( (s) => s.closeConfirmDeleteCandidate, ); // Resolution dialog state const resolveThreadId = useUIStore((s) => s.resolveThreadId); const resolveCandidateId = useUIStore((s) => s.resolveCandidateId); const closeResolveDialog = useUIStore((s) => s.closeResolveDialog); // Onboarding — only check when authenticated (endpoint requires auth) const { data: onboardingComplete, isLoading: onboardingLoading } = useOnboardingComplete(); const [wizardDismissed, setWizardDismissed] = useState(false); // Don't show onboarding wizard until user has created an account const showWizard = !onboardingLoading && onboardingComplete !== "true" && !wizardDismissed && isAuthenticated; const isItemPanelOpen = panelMode !== "closed"; const isCandidatePanelOpen = candidatePanelMode !== "closed"; // Route matching for contextual behavior const matchRoute = useMatchRoute(); const threadMatch = matchRoute({ to: "/threads/$threadId", fuzzy: true, }) as { threadId?: string } | false; const currentThreadId = threadMatch ? Number(threadMatch.threadId) : null; const isDashboard = !!matchRoute({ to: "/" }); const isCollection = !!matchRoute({ to: "/collection", fuzzy: true }); const isSetupDetail = !!matchRoute({ to: "/setups/$setupId", fuzzy: true }); // Determine TotalsBar props based on current route const _totalsBarProps = isDashboard ? { stats: [] as Array<{ label: string; value: string }> } // Title only, no stats, no link : isSetupDetail ? { linkTo: "/" } // Setup detail will render its own local bar; root bar just has link : { linkTo: "/" }; // All other pages: default stats + link to dashboard // On dashboard, don't show the default global stats - pass empty stats // On collection, let TotalsBar fetch its own global stats (default behavior) const finalTotalsProps = isDashboard ? { stats: [] as Array<{ label: string; value: string }> } : isCollection ? { linkTo: "/" } : { linkTo: "/" }; // FAB visibility: only show on /collection route when gear tab is active const collectionSearch = matchRoute({ to: "/collection" }) as | { tab?: string } | false; const showFab = isCollection && (!collectionSearch || !(collectionSearch as Record).tab || (collectionSearch as Record).tab === "gear"); // Show loading while checking auth if (authLoading) { return (
); } // Redirect unauthenticated users to login (server-side OIDC route) // Allow public routes through without auth const isPublicRoute = location.pathname.startsWith("/users/") || location.pathname === "/login"; if (!isAuthenticated && !isPublicRoute) { window.location.href = "/login"; return (

Redirecting to login...

); } // Show loading while checking onboarding status if (onboardingLoading) { return (
); } return (
{/* Item Slide-out Panel */} {panelMode === "add" && } {panelMode === "edit" && ( )} {/* Candidate Slide-out Panel */} {currentThreadId != null && ( {candidatePanelMode === "add" && ( )} {candidatePanelMode === "edit" && ( )} )} {/* Item Confirm Delete Dialog */} {/* External Link Confirmation Dialog */} {/* Candidate Delete Confirm Dialog */} {confirmDeleteCandidateId != null && currentThreadId != null && ( )} {/* Resolution Confirm Dialog */} {resolveThreadId != null && resolveCandidateId != null && ( { closeResolveDialog(); navigate({ to: "/collection", search: { tab: "planning" } }); }} /> )} {/* Floating Add Button - only on collection gear tab */} {showFab && isAuthenticated && ( )} {/* Onboarding Wizard */} {showWizard && ( setWizardDismissed(true)} /> )}
); } function CandidateDeleteDialog({ candidateId, threadId, onClose, }: { candidateId: number; threadId: number; onClose: () => void; }) { const deleteCandidate = useDeleteCandidate(threadId); const { data: thread } = useThread(threadId); const candidate = thread?.candidates.find((c) => c.id === candidateId); const candidateName = candidate?.name ?? "this candidate"; function handleDelete() { deleteCandidate.mutate(candidateId, { onSuccess: () => onClose(), }); } return (
{ if (e.key === "Escape") onClose(); }} />

Delete Candidate

Are you sure you want to delete{" "} {candidateName}? This action cannot be undone.

); } function ResolveDialog({ threadId, candidateId, onClose, onResolved, }: { threadId: number; candidateId: number; onClose: () => void; onResolved: () => void; }) { const resolveThread = useResolveThread(); const { data: thread } = useThread(threadId); const candidate = thread?.candidates.find((c) => c.id === candidateId); const candidateName = candidate?.name ?? "this candidate"; function handleResolve() { resolveThread.mutate( { threadId, candidateId }, { onSuccess: () => onResolved() }, ); } return (
{ if (e.key === "Escape") onClose(); }} />

Pick Winner

Pick {candidateName} as the winner? This will add it to your collection and archive the thread.

); }