import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Link, useLocation } from "react-router-dom"; import { CourseCover, SegmentedToggle } from "@stc/ui"; import { courseHref, toast, useLibrary, useLook, useMe, useSearch, useToast } from "../api"; import { DirI, Down, MenuI, SearchI } from "../icons"; export interface HeaderCourse { id: string; title: string; coverUrl: string | null; accent: string | null } const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; /** * Course switcher, search, Reading/Work and the account link, portalled into the Phase 0 header (`.stc-top`). * Rendered only after mount, so server-side rendering of a page skips it. */ export function HeaderExtras({ course, lookMode, outline }: { course: HeaderCourse | null; /** "toggle": show Reading/Work; "work": shown but fixed to Work (exercise pages); null: hidden. */ lookMode: "toggle" | "work" | null; outline?: { open: boolean; toggle: () => void } | null; }) { const [host, setHost] = useState(null); useIsoLayoutEffect(() => { setHost(document.querySelector(".stc-top")); }, []); const msg = useToast(); if (!host) return null; return ( <> {createPortal(
{outline ? ( ) : null} {lookMode ? : null}
, host, )} {msg ?
{msg}
: null} ); } function LookToggle({ mode }: { mode: "toggle" | "work" }) { const [look, setLook] = useLook(); const value = mode === "work" ? "work" : look; return ( { setLook(v as "reading" | "work"); if (mode === "work" && v === "reading") toast("Exercises always use Work. Reading applies to text lessons."); }} /> ); } function SearchBox({ placeholder }: { placeholder: string }) { const [q, setQ] = useSearch(); const loc = useLocation(); const path = useRef(loc.pathname.split("/").slice(0, 3).join("/")); // Clear the query when moving to another course or to the library. useEffect(() => { const p = loc.pathname.split("/").slice(0, 3).join("/"); if (p !== path.current) { path.current = p; setQ(""); } }, [loc.pathname, setQ]); return ( ); } function CourseSwitcher({ course }: { course: HeaderCourse | null }) { const [open, setOpen] = useState(false); const lib = useLibrary(); const ref = useRef(null); const loc = useLocation(); useEffect(() => { setOpen(false); }, [loc.pathname]); useEffect(() => { if (!open) return; const onDoc = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); ref.current?.querySelector("button")?.focus(); } }; document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); ref.current?.querySelector(".lr-menu a")?.focus(); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; }, [open]); const n = lib?.courses.length ?? 0; return (
{open ? (
  • Library{n === 1 ? "1 course" : `${n} courses`}
  • {(lib?.courses ?? []).map((c) => (
  • {c.title}{c.authors.join(", ") || (c.source === "authored" ? "Authored course" : c.source ? `Imported (${c.source})` : "")}
  • ))}
) : null}
); } function AccountLink() { const { loaded, user } = useMe(); if (!loaded) return null; return user ? {user.name || user.email} : Sign in; }