"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useMemo, useState, useSyncExternalStore, type ReactNode } from "react"; import { SidebarChatSection } from "@/components/chat/SidebarChatSection"; import { ChatIcon, ChevronLeftIcon, ChevronRightIcon, CompassIcon, FlameIcon, FolderIcon, GearIcon, LayersIcon, MoonIcon, PlusIcon, SparklesIcon, SunIcon, } from "@/components/icons"; import { useTheme } from "@/lib/theme"; import { createFolder, folderStore } from "@/lib/folders"; import { haptic } from "@/lib/haptics"; import { IS_DEV_INSTANCE } from "@/lib/dev-mode"; import { useI18n } from "@/lib/i18n"; import { ALL_FOLDERS, folderSelection, useSelectedFolder } from "@/lib/nav"; import { UNORGANIZED_ID } from "@/lib/schemas"; import { deckStore } from "@/lib/storage"; import { openSettings } from "@/lib/ui-events"; import { cn } from "@/lib/utils"; const COLLAPSE_KEY = "freezmo.sidebarCollapsed"; /** * Desktop navigation rail (lg and up). Phones use the bottom tab bar instead — * there is no drawer any more, so this is the only consumer of the rail. */ function NavContent({ collapsed = false }: { collapsed?: boolean }) { const pathname = usePathname(); const onChat = pathname === "/chat"; const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot); const folders = useSyncExternalStore( folderStore.subscribe, folderStore.getSnapshot, folderStore.getServerSnapshot, ); const selected = useSelectedFolder(); const { theme, toggle } = useTheme(); const { t } = useI18n(); const [creating, setCreating] = useState(false); const [draft, setDraft] = useState(""); const [error, setError] = useState(null); const sortedFolders = useMemo( () => [...folders].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })), [folders], ); const submit = () => { const result = createFolder(draft); if ("error" in result) { setError(result.error); return; } folderSelection.set(result.folder.id); setDraft(""); setError(null); setCreating(false); }; /** Counts of 0 are noise — only show a count when there is something in it. */ const count = (value: number) => !collapsed && value > 0 ? {value} : null; /** One shared view row (All decks / Unorganized). */ const viewRow = (id: string, label: string, icon: ReactNode, trailing: ReactNode) => ( ); const brandMark = ( // eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed ); return (
{/* brand ---------------------------------------------------------- */}
{brandMark} {!collapsed ? ( <> Freezmo {IS_DEV_INSTANCE ? ( DEV ) : null} ) : null}
{/* collapse / expand: its own row directly below the logo, in both states, so the arrow never jumps around the rail. */} {/* new deck: a button, not a nav row — it stays visible when the library is already full (see .nav-action in globals.css) --------- */} {!collapsed ? {t("common.newDeck")} : null} {/* new homework: the same button ------------------------------------ */} {!collapsed ? {t("homework.nav")} : null} {/* chat with your notes --------------------------------------------- */} {!collapsed ? {t("chat.nav")} : null} {/* On the Chat route the chat list takes the deck navigation's place. */} {onChat ? : null} {/* footer ---------------------------------------------------------- */}
{!collapsed ? {t("nav.stats")} : null} {!collapsed ? {t("nav.exam")} : null} {/* Frutiger Aero has no light/dark flip (see the theme picker in Settings), so this row goes away while it is on. */} {theme === "aero" ? null : ( )}
); } /* ------------------------------------------------------------------ */ /* Collapsed state (desktop rail) */ /* ------------------------------------------------------------------ */ let collapsed = false; const collapseListeners = new Set<() => void>(); export const sidebarCollapse = { subscribe(listener: () => void): () => void { collapseListeners.add(listener); return () => collapseListeners.delete(listener); }, getSnapshot(): boolean { return collapsed; }, getServerSnapshot(): boolean { return false; }, set(value: boolean): void { collapsed = value; try { window.localStorage.setItem(COLLAPSE_KEY, value ? "1" : "0"); } catch { /* private mode */ } collapseListeners.forEach((listener) => listener()); }, hydrate(): void { try { collapsed = window.localStorage.getItem(COLLAPSE_KEY) === "1"; } catch { collapsed = false; } collapseListeners.forEach((listener) => listener()); }, }; function useCollapsed(): boolean { return useSyncExternalStore( sidebarCollapse.subscribe, sidebarCollapse.getSnapshot, sidebarCollapse.getServerSnapshot, ); } /* ------------------------------------------------------------------ */ /* Desktop rail */ /* ------------------------------------------------------------------ */ export function Sidebar() { const isCollapsed = useCollapsed(); const { t } = useI18n(); return ( ); } /** Spacer so page content clears the fixed rail (desktop only). */ export function SidebarOffset() { const isCollapsed = useCollapsed(); return (
); }