"use client"; import Image from "next/image"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Bell, Bookmark, BookOpen, Check, ChevronLeft, ChevronRight, Command, LogOut, Menu, RefreshCw, Search, Settings, StickyNote, UserRound, WifiOff, X, } from "lucide-react"; import { Suspense, useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; import { useWorkspaceOverlay } from "./useWorkspaceOverlay"; import { useTheme } from "next-themes"; import { AnimatePresence, LayoutGroup, motion, useReducedMotion, } from "framer-motion"; import { hasLiveSession } from "@/lib/api/mode"; import { isAuthenticationRequired } from "@/lib/auth/session-requirement"; import { useAuth } from "@/lib/auth/useAuth"; import { cn } from "@/lib/utils"; import { getBillingMe } from "@/lib/api/users"; import { isActivePaidPlanKey } from "@/lib/billing/activePlans"; import type { BillingMe } from "@/lib/api/types"; import { StudentOsAssistant } from "./StudentOsAssistant"; import { StudentOsPageSkeleton } from "./StudentOsStates"; import { StudentWorkspaceProvider, useStudentWorkspace, } from "./StudentWorkspaceProvider"; import { studentLibraryDestination, studentLibraryNavigation, studentPrimaryNavigation, studentSearchNavigation, } from "./studentNavigation"; import { popoverVariants, routeVariants } from "./motion"; const springSoft = { type: "spring" as const, stiffness: 420, damping: 36, mass: 0.85 }; const springSnappy = { type: "spring" as const, stiffness: 520, damping: 38, mass: 0.7 }; const easeOutExpo = [0.16, 1, 0.3, 1] as const; const FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'; function useOverlayFocus( containerRef: React.RefObject, open: boolean, onClose: () => void, ) { const close = useEffectEvent(onClose); useEffect(() => { if (!open) return; const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null; const container = containerRef.current; const focusables = () => Array.from(container?.querySelectorAll(FOCUSABLE_SELECTOR) ?? []).filter( (element) => element.offsetParent !== null && element.tabIndex >= 0, ); const first = focusables()[0]; (first ?? container)?.focus({ preventScroll: true }); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); close(); return; } if (event.key !== "Tab") return; const items = focusables(); if (!items.length) return; const current = document.activeElement; const index = items.indexOf(current as HTMLElement); const next = event.shiftKey ? items[index <= 0 ? items.length - 1 : index - 1] : items[index === items.length - 1 ? 0 : index + 1]; event.preventDefault(); next.focus(); }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); previous?.focus({ preventScroll: true }); }; }, [containerRef, open]); } // Bottom-right DocDoe guide (OURDOCDOE). Toggle only if product wants it off. // Present on every student route; each student can still disable it in Settings. const ASSISTANT_WIDGET_ENABLED = true; function isActive(pathname: string, href: string) { return pathname === href || pathname.startsWith(`${href}/`); } function StudentOsLogo({ compact = false }: { compact?: boolean }) { return ( {compact {compact ? null : ( DocDoe Your study workspace )} ); } function NavigationList({ pathname, onNavigate, libraryOnly = false, compact = false, }: { pathname: string; onNavigate?: () => void; libraryOnly?: boolean; compact?: boolean; }) { function renderLink(item: (typeof studentSearchNavigation)[number]) { const active = isActive(pathname, item.href); const Icon = item.icon; return ( {compact ? {item.label} : {item.label}} ); } return ( ); } function Sidebar({ pathname, collapsed, onToggleCollapsed, }: { pathname: string; collapsed: boolean; onToggleCollapsed: () => void; }) { const { user } = useAuth(); const { learningState } = useStudentWorkspace(); const [billing, setBilling] = useState(null); const name = user?.name?.trim() || "Student"; const classLine = [learningState?.profile?.class_level, learningState?.profile?.board] .filter(Boolean) .join(" · ") || "Complete your study setup"; const isPaid = billing ? isActivePaidPlanKey(billing.selected_plan) : false; useEffect(() => { let cancelled = false; void getBillingMe() .then((value) => { if (!cancelled) setBilling(value); }) .catch(() => undefined); return () => { cancelled = true; }; }, []); return ( ); } function SearchPalette({ onClose }: { onClose: () => void }) { const router = useRouter(); const { data } = useStudentWorkspace(); const [query, setQuery] = useState(""); const results = useMemo(() => { const pages = studentSearchNavigation.map((item) => ({ label: item.label, detail: "Page", href: item.href, icon: item.icon, })); const classes = data.classes.map((item) => ({ label: item.name, detail: item.subject, href: item.href || "/classes", icon: BookOpen, })); const notes = data.notes.map((item) => ({ label: item.title, detail: item.subject || "Note", href: `/notes?note=${encodeURIComponent(item.id)}`, icon: StickyNote, })); const bookmarks = data.bookmarks.map((item) => ({ label: item.title, detail: "Bookmark", href: item.href, icon: Bookmark, })); const all = [...pages, ...classes, ...notes, ...bookmarks]; const normalized = query.trim().toLowerCase(); return normalized ? all .filter((item) => `${item.label} ${item.detail}`.toLowerCase().includes(normalized), ) .slice(0, 12) : all.slice(0, 8); }, [data.bookmarks, data.classes, data.notes, query]); const reduceMotion = useReducedMotion(); const dialogRef = useRef(null); useOverlayFocus(dialogRef, true, onClose); return (
{/* Scrim fades via opacity on a static colour. Animating `background-color` forced a full-viewport paint every frame. */}
setQuery(event.target.value)} aria-label="Search pages, classes, notes, and bookmarks" placeholder="Search pages, classes, notes…" className="h-14 min-w-0 flex-1 bg-transparent text-[16px] text-dd-ink outline-none placeholder:text-dd-muted sm:text-sm" />
{results.length > 0 ? ( results.map((item, index) => { const Icon = item.icon; return ( { router.push(item.href); onClose(); }} className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left hover:bg-dd-canvas" initial={reduceMotion ? false : { opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: Math.min(index, 8) * 0.03, duration: 0.22, ease: easeOutExpo }} > {item.label} {item.detail} ); }) ) : (

No matching study item

Try a subject, note title, or page name.

)}
Press Ctrl K anywhere to search
); } function Header({ pathname, onOpenMenu, onOpenSearch, }: { pathname: string; onOpenMenu: () => void; onOpenSearch: () => void; }) { const router = useRouter(); const { user, logout } = useAuth(); const { data, status, updateWorkspace, refresh } = useStudentWorkspace(); const [notificationsOpen, setNotificationsOpen] = useState(false); const [profileOpen, setProfileOpen] = useState(false); const reduceMotion = useReducedMotion(); const notificationsRef = useWorkspaceOverlay(notificationsOpen, () => setNotificationsOpen(false)); const profileRef = useWorkspaceOverlay(profileOpen, () => setProfileOpen(false)); const unread = data.notifications.filter((item) => !item.read_at).length; const page = studentSearchNavigation.find((item) => isActive(pathname, item.href), ); const name = user?.name?.trim() || "Student"; function markAllRead() { const now = new Date().toISOString(); void updateWorkspace((current) => ({ ...current, notifications: current.notifications.map((item) => ({ ...item, read_at: item.read_at ?? now, })), })).catch(() => undefined); } return (
{page?.label ?? "DocDoe"}
{status === "offline" || status === "conflict" ? ( ) : null} {status === "saving" ? ( ) : null}
{notificationsOpen ? (

Notifications

Only real workspace updates appear here

{unread > 0 ? ( ) : null}
{data.notifications.length ? ( data.notifications.slice(0, 12).map((item) => ( )) ) : (

You're all caught up

Job completions and study reminders will appear here.

)}
setNotificationsOpen(false)} className="flex h-10 items-center justify-center border-t border-dd-line text-[10px] font-bold text-dd-ink" > View all notifications
) : null}
{profileOpen ? (

{name}

{user?.email ?? "Student account"}

setProfileOpen(false)} role="menuitem" className="mt-1 flex min-h-11 items-center gap-2 rounded-xl px-3 py-2.5 text-xs font-semibold text-dd-ink hover:bg-dd-canvas" > Profile & settings
) : null}
); } function MobileDrawer({ open, pathname, onClose, libraryOnly = false, }: { open: boolean; pathname: string; onClose: () => void; libraryOnly?: boolean; }) { const reduceMotion = useReducedMotion(); const drawerRef = useRef(null); useOverlayFocus(drawerRef, open, onClose); return ( {open ? (
{libraryOnly ? (

Your study space

Library

) : ( )}
Profile & settings
) : null}
); } function MobileBottomBar({ pathname, onLibrary, }: { pathname: string; onLibrary: () => void; }) { const reduceMotion = useReducedMotion(); const LibraryIcon = studentLibraryDestination.icon; return ( ); } function StudentOsFrame({ children }: { children: React.ReactNode }) { const router = useRouter(); const { setTheme } = useTheme(); const pathname = usePathname() ?? "/home"; const searchParams = useSearchParams(); const page = studentSearchNavigation.find((item) => isActive(pathname, item.href), ); const { isLoading: isAuthLoading } = useAuth(); const reduceMotion = useReducedMotion(); const [menuPathname, setMenuPathname] = useState(null); const [libraryPathname, setLibraryPathname] = useState(null); const [searchPathname, setSearchPathname] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const menuOpen = menuPathname === pathname; const libraryOpen = libraryPathname === pathname; const searchOpen = searchPathname === pathname; const mainRef = useRef(null); const previousPathname = useRef(pathname); const unauthenticated = isAuthenticationRequired() || !hasLiveSession(); const focusedTuition = pathname === "/tuition" && searchParams.get("start") === "1"; useEffect(() => { setTheme("light"); }, [setTheme]); useEffect(() => { if (!menuOpen && !libraryOpen && !searchOpen) return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previousOverflow; }; }, [libraryOpen, menuOpen, searchOpen]); useEffect(() => { if (!isAuthLoading && unauthenticated) router.replace(`/login?next=${encodeURIComponent(pathname)}`); }, [isAuthLoading, pathname, router, unauthenticated]); useEffect(() => { function handleKeyDown(event: KeyboardEvent) { const target = event.target instanceof HTMLElement ? event.target : null; const isEditing = Boolean( target?.isContentEditable || target?.matches("input, textarea, select"), ); if ( !event.isComposing && !isEditing && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k" ) { event.preventDefault(); setSearchPathname(pathname); } if (event.key === "Escape") { setSearchPathname(null); setMenuPathname(null); setLibraryPathname(null); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [pathname]); useEffect(() => { if (previousPathname.current === pathname) return; previousPathname.current = pathname; const frame = window.requestAnimationFrame(() => { mainRef.current?.focus({ preventScroll: true }); }); return () => window.cancelAnimationFrame(frame); }, [pathname]); const blockChildren = isAuthLoading || unauthenticated; return (
{!focusedTuition ? ( setSidebarCollapsed((value) => !value)} /> ) : null} {!focusedTuition ? (
setMenuPathname(pathname)} onOpenSearch={() => setSearchPathname(pathname)} /> ) : null} setMenuPathname(null)} /> setLibraryPathname(null)} libraryOnly /> {searchOpen ? ( setSearchPathname(null)} /> ) : null}
}> {blockChildren ? ( ) : ( /* `mode="wait"` gives the outgoing route a real exit instead of an abrupt swap, which is what makes navigation feel connected. One motion node per route, so it stays inside the budget. */ {children} )}
{!focusedTuition ? ( setLibraryPathname(pathname)} /> ) : null} {ASSISTANT_WIDGET_ENABLED && !focusedTuition ? ( ) : null}
); } export function StudentOsShell({ children }: { children: React.ReactNode }) { return ( }> {children} ); }