/** * Shared layout components for consistent page structure. */ import { ExternalLink, type LucideIcon, Menu } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useVersionCheck } from "../hooks/useVersionCheck"; import { isNavActive, NAV_LINKS } from "./navigation"; import { StatusBadgeIndicator } from "./StatusIndicator"; // ============================================================================ // Page Header // ============================================================================ interface PageHeaderProps { icon: LucideIcon | React.FC<{ className?: string }>; title: string; subtitle: string; badge?: string; statusIndicator?: React.ReactNode; actions?: React.ReactNode; showVersionFooter?: boolean; navOpen?: boolean; onNavOpenChange?: (open: boolean) => void; } export const PageHeader: React.FC = ({ icon: Icon, title, subtitle, badge, statusIndicator, actions, showVersionFooter = true, navOpen: controlledNavOpen, onNavOpenChange, }) => { const location = useLocation(); const navigate = useNavigate(); const [internalNavOpen, setInternalNavOpen] = useState(false); const navOpen = controlledNavOpen ?? internalNavOpen; const setNavOpen = onNavOpenChange ?? setInternalNavOpen; const { version, updateAvailable } = useVersionCheck(); const handleNavClick = (to: string, activePaths?: string[]) => { if (isNavActive(location.pathname, to, activePaths)) { setNavOpen(false); return; } setNavOpen(false); setTimeout(() => navigate(to), 150); }; return (
JobOps {showVersionFooter && (
Version {version} {updateAvailable && (

Update available

)}
)}
{title}
{subtitle}
{badge && ( {badge} )} {statusIndicator}
{actions}
); }; export const StatusIndicator = StatusBadgeIndicator; // ============================================================================ // Split Layout (List + Detail panels) // ============================================================================ interface SplitLayoutProps { children: React.ReactNode; className?: string; } export const SplitLayout: React.FC = ({ children, className, }) => (
{children}
); // ============================================================================ // List Panel (left side of split) // ============================================================================ interface ListPanelProps { children: React.ReactNode; header?: React.ReactNode; footer?: React.ReactNode; className?: string; } export const ListPanel: React.FC = ({ children, header, footer, className, }) => (
{header && (
{header}
)}
{children}
{footer && (
{footer}
)}
); // ============================================================================ // List Item (clickable row in list) // ============================================================================ interface ListItemProps { selected?: boolean; onClick?: () => void; children: React.ReactNode; className?: string; } export const ListItem: React.FC = ({ selected, onClick, children, className, }) => ( ); // ============================================================================ // Detail Panel (right side of split) // ============================================================================ interface DetailPanelProps { children: React.ReactNode; className?: string; sticky?: boolean; } export const DetailPanel: React.FC = ({ children, className, sticky = true, }) => (
{children}
); // ============================================================================ // Empty State // ============================================================================ interface EmptyStateProps { icon?: LucideIcon; title: string; description?: string; action?: React.ReactNode; } export const EmptyState: React.FC = ({ icon: Icon, title, description, action, }) => (
{Icon && }
{title}
{description && (

{description}

)} {action &&
{action}
}
); // ============================================================================ // Score Meter // ============================================================================ interface ScoreMeterProps { score: number | null; showLabel?: boolean; } const getScoreTokens = (score: number) => { if (score >= 90) return { bar: "bg-emerald-500/80" }; if (score >= 70) return { bar: "bg-amber-500/80" }; if (score >= 50) return { bar: "bg-orange-500/80" }; return { bar: "bg-rose-500/80" }; }; export const ScoreMeter: React.FC = ({ score, showLabel = true, }) => { if (score == null) { return Not scored; } const tokens = getScoreTokens(score); return (
{showLabel && ( {score}% )}
); }; // ============================================================================ // Full Height Split Layout (for pages like VisaSponsors that use full viewport) // ============================================================================ interface FullHeightSplitProps { sidebar: React.ReactNode; sidebarWidth?: string; children: React.ReactNode; } export const FullHeightSplit: React.FC = ({ sidebar, sidebarWidth = "lg:w-[420px]", children, }) => (
{sidebar}
{children}
); // ============================================================================ // Section Card (for forms, stats, etc.) // ============================================================================ interface SectionCardProps { children: React.ReactNode; className?: string; } export const SectionCard: React.FC = ({ children, className, }) => (
{children}
); // ============================================================================ // Page Main Content Wrapper // ============================================================================ interface PageMainProps { children: React.ReactNode; className?: string; } export const PageMain: React.FC = ({ children, className }) => (
{children}
);