"use client"; import Link from "next/link"; import { useMemo, useState, useSyncExternalStore, type ReactNode } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronLeftIcon, ChevronRightIcon, FolderIcon, GearIcon, LayersIcon, MoonIcon, PlusIcon, SparklesIcon, SunIcon, } from "@/components/icons"; import { useTheme } from "@/components/ThemeToggle"; import { createFolder, folderStore } from "@/lib/folders"; import { useI18n } from "@/lib/i18n"; import { ALL_FOLDERS, folderSelection, sidebarOpen, useSelectedFolder, useSidebarOpen } 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"; /** Shared nav content for the desktop rail and the mobile drawer. */ function NavContent({ collapsed = false, onNavigate, desktop = false, }: { collapsed?: boolean; onNavigate?: () => void; desktop?: boolean; }) { 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 counts = useMemo(() => { const map: Record = { [UNORGANIZED_ID]: 0 }; for (const folder of sortedFolders) map[folder.id] = 0; for (const deck of decks) { const key = deck.folderId && map[deck.folderId] !== undefined ? deck.folderId : UNORGANIZED_ID; map[key] = (map[key] ?? 0) + 1; } return map; }, [decks, sortedFolders]); const submit = () => { const result = createFolder(draft); if ("error" in result) { setError(result.error); return; } folderSelection.set(result.folder.id); setDraft(""); setError(null); setCreating(false); }; const select = (id: string) => { folderSelection.set(id); onNavigate?.(); }; /** 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) for both nav layouts. */ const viewRow = (id: string, label: string, icon: ReactNode, trailing: ReactNode) => ( ); return (
{/* brand ---------------------------------------------------------- */}
{/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */} {!collapsed ? ( Freezmo ) : null} {!collapsed ? ( ) : null}
{/* new deck: a quiet row, not a competing primary button ---------- */} {!collapsed ? {t("common.newDeck")} : null} {/* make my homework: same quiet row --------------------------------- */} {!collapsed ? {t("homework.nav")} : null} {/* folders --------------------------------------------------------- */} {/* Mobile keeps the original layout: one Folders label above the list. */} {!desktop && !collapsed ? (
{t("common.folders")}
) : null} {/* footer ---------------------------------------------------------- */}
); } /* ------------------------------------------------------------------ */ /* Collapsed state (shared between rail and drawer) */ /* ------------------------------------------------------------------ */ 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 + mobile drawer */ /* ------------------------------------------------------------------ */ export function Sidebar() { const isCollapsed = useCollapsed(); const { t } = useI18n(); return ( ); } export function MobileSidebar() { const open = useSidebarOpen(); const { t } = useI18n(); return ( {open ? (