import { LanguageSwitcher } from "@/components/language-switcher";
import { SiteFooter } from "@/components/site-footer";
import { TrialUrgencyBanner } from "@/components/trial-urgency-banner";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { KEYS_5 } from "@/lib/keys";
import { capture, resetUser } from "@/lib/posthog";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Separator,
Sheet,
SheetContent,
SheetTitle,
Skeleton,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, Outlet, createFileRoute, useNavigate, useRouterState } from "@tanstack/react-router";
import {
BarChart3,
BookOpen,
CalendarDays,
ChevronsUpDown,
Copy,
CreditCard,
FlaskConical,
History,
LayoutDashboard,
Library,
LogOut,
Mail,
Menu,
Moon,
PanelLeftClose,
PanelLeftOpen,
Receipt,
Search,
Settings,
Share2,
Shield,
Sparkles,
Sun,
Users,
} from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
});
// ─── NAV SECTIONS ─────────────────────────────────────────────────────────────
type NavItem = {
to: string;
labelKey: string;
icon: React.ComponentType<{ className?: string }>;
exact?: boolean;
};
const mainMenuItems: readonly NavItem[] = [
{ to: "/dashboard", labelKey: "nav.dashboard", icon: LayoutDashboard, exact: true },
{ to: "/dashboard/search", labelKey: "nav.search", icon: Search },
{ to: "/dashboard/catalog", labelKey: "nav.catalog", icon: Library },
{ to: "/dashboard/history", labelKey: "nav.history", icon: History },
];
const accountItems: readonly NavItem[] = [
{ to: "/dashboard/subscription", labelKey: "nav.subscription", icon: CreditCard },
{ to: "/dashboard/billing", labelKey: "nav.billing", icon: Receipt },
{ to: "/dashboard/settings", labelKey: "nav.settings", icon: Settings },
];
const supportItems: readonly NavItem[] = [
{ to: "/dashboard/changelog", labelKey: "nav.changelog", icon: CalendarDays },
{ to: "/contact", labelKey: "nav.contact", icon: Mail },
{ to: "/blog", labelKey: "nav.blog", icon: BookOpen },
];
const adminItems: readonly NavItem[] = [
{ to: "/dashboard/admin", labelKey: "nav.adminPanel", icon: Shield },
{ to: "/dashboard/admin/users", labelKey: "nav.users", icon: Users },
{ to: "/dashboard/admin/analytics", labelKey: "nav.analytics", icon: BarChart3 },
{ to: "/dashboard/admin/copy-logs", labelKey: "nav.copyLogs", icon: Copy },
{ to: "/dashboard/admin/referrals", labelKey: "nav.referrals", icon: Share2 },
{ to: "/dashboard/service-test", labelKey: "nav.serviceTest", icon: FlaskConical },
];
// ─── HELPERS ──────────────────────────────────────────────────────────────────
function NavSection({ title, collapsed }: { title: string; collapsed: boolean }) {
if (collapsed) {
return ;
}
return (
{title}
);
}
function NavLink({
to,
label,
icon: Icon,
collapsed,
onClick,
exact,
}: {
to: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
collapsed: boolean;
onClick?: () => void;
exact?: boolean;
}) {
const link = (
{/* Left accent bar — visible when active (both collapsed & expanded) */}
{!collapsed && {label} }
);
if (collapsed) {
return (
{link}
{label}
);
}
return link;
}
// ─── LAYOUT ───────────────────────────────────────────────────────────────────
function DashboardLayout() {
const { t } = useTranslation();
const { user, isLoading, signOut, isAdmin } = useAuth();
const navigate = useNavigate();
const pathname = useRouterState({ select: (s) => s.location.pathname });
const [mobileOpen, setMobileOpen] = useState(false);
const [collapsed, setCollapsed] = useState(() => getUserSettings().sidebarCollapsed ?? false);
const [isDark, setIsDark] = useState(() => {
const theme = getUserSettings().theme ?? "dark";
if (theme === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return theme === "dark";
});
// Shared with TrialUrgencyBanner via the same query key — drives the upsell button.
const { data: subData } = useQuery({
queryKey: ["subscription", "me"],
queryFn: () =>
api.get<{ subscription: { status: string } | null; eligibleForTrial: boolean }>(
"/subscriptions/me",
),
enabled: !!user,
});
const hasActivePlan = subData?.subscription?.status === "active";
// Redirect unauthenticated users without mutating router state during render.
useEffect(() => {
if (!isLoading && !user) {
navigate({ to: "/login" });
}
}, [isLoading, user, navigate]);
const handleSignOut = () => {
capture("user_logged_out");
resetUser();
signOut();
};
const toggleTheme = () => {
const next = isDark ? "light" : "dark";
document.documentElement.classList.toggle("dark", next === "dark");
setUserSetting("theme", next);
setIsDark(next === "dark");
};
const toggleCollapsed = () => {
const next = !collapsed;
setCollapsed(next);
setUserSetting("sidebarCollapsed", next);
};
if (isLoading) {
return (
{KEYS_5.map((__k) => (
))}
);
}
if (!user) {
return null;
}
const initials = user.name
? user.name
.split(" ")
.map((w) => w[0])
.join("")
.toUpperCase()
.slice(0, 2)
: "?";
// Capture narrowed (non-null) user fields for the nested ProfileMenu closure.
const userName = user.name;
const userEmail = user.email;
// Current page title for the header — longest matching nav `to` wins.
const allNavItems = [...mainMenuItems, ...accountItems, ...supportItems, ...adminItems];
const currentItem = allNavItems
.filter((i) =>
i.exact ? pathname === i.to : pathname === i.to || pathname.startsWith(`${i.to}/`),
)
.sort((a, b) => b.to.length - a.to.length)[0];
const pageTitle = currentItem ? t(currentItem.labelKey) : t("nav.dashboard");
// Upgrade CTA shown above the profile — only for users without a paid plan.
function UpgradeBadge({
collapsed: isCollapsed,
onClick,
}: {
collapsed: boolean;
onClick?: () => void;
}) {
if (hasActivePlan) return null;
if (isCollapsed) {
return (
{t("nav.upgradeAccount")}
);
}
return (
{t("nav.upgradeAccount")}
);
}
// Profile menu — replaces the old "click-to-logout" trap with a real menu.
function ProfileMenu({
collapsed: isCollapsed,
onItemSelect,
}: {
collapsed: boolean;
onItemSelect?: () => void;
}) {
return (
{initials}
{!isCollapsed && (
<>
>
)}
{userName}
{userEmail}
{t("nav.settings")}
{t("nav.subscription")}
{t("nav.logout")}
);
}
// Sidebar content (shared between desktop & mobile)
function SidebarNav({ onNavigate }: { onNavigate?: () => void }) {
const isCollapsed = collapsed && !onNavigate;
return (
<>
{mainMenuItems.map((item) => (
))}
{accountItems.map((item) => (
))}
{supportItems.map((item) => (
))}
{isAdmin && (
<>
{adminItems.map((item) => (
))}
>
)}
>
);
}
return (
{/* ─── Desktop Sidebar ──────────────────────────────────────────── */}
{/* Logo + Collapse */}
{!collapsed && (
SASE
)}
{collapsed ? (
) : (
)}
{/* Navigation */}
{/* User Profile - Bottom */}
{/* ─── Main Content Area ────────────────────────────────────────── */}
{/* min-w-0: lets this flex column shrink below its content's intrinsic
width so wide children (e.g. the subscription feature-comparison
table) scroll inside their own overflow-x container instead of
forcing the whole mobile layout viewport wider than the device. */}
{/* Top Header Bar */}
{/* Page Content */}
{/* Footer */}
{/* ─── Mobile Nav Drawer (Radix Dialog: focus-trap, Esc, scroll-lock) ─── */}
setMobileOpen(false)}
>
SASE
setMobileOpen(false)} />
setMobileOpen(false)} />
setMobileOpen(false)} />
);
}