import { createRootRoute, Outlet, useMatchRoute, useNavigate, } 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 { 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, }); function RootLayout() { const navigate = useNavigate(); // 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 const { data: onboardingComplete, isLoading: onboardingLoading } = useOnboardingComplete(); const [wizardDismissed, setWizardDismissed] = useState(false); const showWizard = !onboardingLoading && onboardingComplete !== "true" && !wizardDismissed; 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 !== "planning"); // Show a minimal loading state 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 && ( )} {/* 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.

); }