| import { useEffect, useRef, useState } from "react"; |
| import { createProject } from "./api"; |
| import { useProject } from "./project"; |
| import { getIdentity, subscribeIdentity } from "./session"; |
| import { ChevronDown } from "./icons"; |
|
|
| |
| |
| |
| export default function ProjectSwitcher() { |
| const { projectId, setProjectId, projects, refresh } = useProject(); |
| const [open, setOpen] = useState(false); |
| const [isAdmin, setIsAdmin] = useState(() => getIdentity().role === "admin"); |
| const [busy, setBusy] = useState(false); |
| const [error, setError] = useState<string | null>(null); |
| const ref = useRef<HTMLDivElement>(null); |
|
|
| |
| |
| useEffect(() => subscribeIdentity(() => setIsAdmin(getIdentity().role === "admin")), []); |
|
|
| |
| useEffect(() => { |
| if (!open) return; |
| function onPointerDown(event: MouseEvent) { |
| if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false); |
| } |
| document.addEventListener("mousedown", onPointerDown); |
| return () => document.removeEventListener("mousedown", onPointerDown); |
| }, [open]); |
|
|
| const current = projects.find((p) => p.project_id === projectId); |
| |
| |
| const currentName = current?.name ?? projectId; |
|
|
| function select(id: string) { |
| setProjectId(id); |
| setOpen(false); |
| } |
|
|
| async function handleNewProject() { |
| const name = window.prompt("New project name")?.trim(); |
| if (!name) return; |
| setBusy(true); |
| setError(null); |
| try { |
| const created = await createProject(name); |
| await refresh(); |
| setProjectId(created.project_id); |
| setOpen(false); |
| } catch (err) { |
| setError(err instanceof Error ? err.message : String(err)); |
| } finally { |
| setBusy(false); |
| } |
| } |
|
|
| return ( |
| <div className="project-switcher" ref={ref}> |
| <button |
| type="button" |
| className="topbar-project" |
| aria-haspopup="menu" |
| aria-expanded={open} |
| onClick={() => setOpen((v) => !v)} |
| title="Switch the open project" |
| > |
| <span className="topbar-project-badge">Project</span> |
| <span className="topbar-project-name">{currentName}</span> |
| {current ? <span className="topbar-project-standards">{current.case_count} cases</span> : null} |
| <span className="project-switcher-caret" aria-hidden> |
| <ChevronDown /> |
| </span> |
| </button> |
| |
| {open ? ( |
| <div className="project-menu" role="menu"> |
| {projects.map((p) => ( |
| <button |
| type="button" |
| key={p.project_id} |
| role="menuitemradio" |
| aria-checked={p.project_id === projectId} |
| className={p.project_id === projectId ? "project-menu-item active" : "project-menu-item"} |
| onClick={() => select(p.project_id)} |
| > |
| <span className="project-menu-name">{p.name}</span> |
| <span className="project-menu-count">{p.case_count}</span> |
| </button> |
| ))} |
| {projects.length === 0 ? <span className="project-menu-empty">No projects yet.</span> : null} |
| {isAdmin ? ( |
| <> |
| <div className="project-menu-sep" role="separator" /> |
| <button |
| type="button" |
| role="menuitem" |
| className="project-menu-new" |
| onClick={handleNewProject} |
| disabled={busy} |
| > |
| {busy ? "Creating…" : "+ New project"} |
| </button> |
| </> |
| ) : null} |
| {error ? ( |
| <span className="project-menu-error" role="alert"> |
| {error} |
| </span> |
| ) : null} |
| </div> |
| ) : null} |
| </div> |
| ); |
| } |
|
|