import { ChevronRight, Clock3, Database, FileText, Folder, FolderGit2, FolderOpen, LogIn, LogOut, PanelLeftClose, PanelLeftOpen, RefreshCw, Shield, UserPlus, Users, } from "lucide-preact"; import { useEffect, useState } from "preact/hooks"; type Role = "admin" | "editor" | "viewer"; type View = "workspace" | "users"; type User = { id: string; email: string; displayName: string; role: Role; createdAt: string; lastSeenAt?: string; }; type DocumentRecord = { path: string; title: string; currentHash: string; updatedAt: string; }; type Workspace = { sourceDir: string; storeDir: string; databasePath: string; documentCount: number; userCount: number; webDistDir: string; }; type WorkspaceResponse = { workspace: Workspace; documents: DocumentRecord[]; }; type Session = { user: User; }; type BrowserItem = { type: "folder" | "document"; name: string; path: string; document?: DocumentRecord; indexDocument?: DocumentRecord; }; type BrowserColumn = { title: string; prefix: string; items: BrowserItem[]; }; const sessionKey = "mdhub.admin.session"; export function App() { const [session, setSession] = useState(() => readSession()); const [activeView, setActiveView] = useState("workspace"); const [sidebarOpen, setSidebarOpen] = useState(true); const [users, setUsers] = useState([]); const [workspace, setWorkspace] = useState(null); const [syncing, setSyncing] = useState(false); const [lastSyncedAt, setLastSyncedAt] = useState(""); const [activePath, setActivePath] = useState(""); const [selectedUserId, setSelectedUserId] = useState(""); async function refresh() { if (!session) { return; } const [usersResponse, workspaceResponse] = await Promise.all([ api<{ users: User[] }>("/api/admin/users"), api("/api/admin/workspace"), ]); setUsers(usersResponse.users); setWorkspace(workspaceResponse); setLastSyncedAt(new Date().toISOString()); } useEffect(() => { void refresh(); }, [session?.user.email]); useEffect(() => { const documents = workspace?.documents ?? []; if ( documents.length > 0 && !isKnownDocumentLocation(documents, activePath) ) { setActivePath(documents[0].path); } }, [workspace?.documents, activePath]); useEffect(() => { if (users.length > 0 && !users.some((user) => user.id === selectedUserId)) { setSelectedUserId(users[0].id); } }, [users, selectedUserId]); async function handleLogin(email: string, displayName: string) { const response = await api<{ user: User }>("/api/admin/login", { method: "POST", body: JSON.stringify({ email, displayName, role: "admin" }), }); const next = { user: response.user }; localStorage.setItem(sessionKey, JSON.stringify(next)); setSession(next); } async function handleCreateUser(input: { email: string; displayName: string; role: Role; }) { const response = await api<{ user: User }>("/api/admin/users", { method: "POST", body: JSON.stringify(input), }); await refresh(); setSelectedUserId(response.user.id); } async function handleSync() { setSyncing(true); try { await api("/api/admin/workspace/sync", { method: "POST" }); await refresh(); } finally { setSyncing(false); } } function logout() { localStorage.removeItem(sessionKey); setSession(null); setUsers([]); setWorkspace(null); setActivePath(""); setSelectedUserId(""); } if (!session) { return ; } const selectedDocument = selectDocument( workspace?.documents ?? [], activePath, ); const selectedUser = users.find((user) => user.id === selectedUserId) ?? users[0]; return (
{activeView === "workspace" ? "Documents" : "Accounts"}
{activeView === "workspace" && ( )} {syncing ? "Syncing" : lastSyncedAt ? `Synced ${formatTime(lastSyncedAt)}` : "Not synced"}
{activeView === "workspace" ? ( ) : ( )}
); } function LoginScreen(props: { onLogin: (email: string, displayName: string) => Promise; }) { const [email, setEmail] = useState("admin@example.com"); const [displayName, setDisplayName] = useState("Admin"); const [error, setError] = useState(""); async function submit(event: Event) { event.preventDefault(); setError(""); try { await props.onLogin(email, displayName); } catch (cause) { setError(cause instanceof Error ? cause.message : "Login failed"); } } return (
); } function WorkspaceView(props: { data: WorkspaceResponse | null; activePath: string; selectedDocument?: DocumentRecord; onSelectPath: (path: string) => void; }) { const documents = props.data?.documents ?? []; const columns = buildDocumentColumns(documents, props.activePath); return (
{columns.map((column) => (
{column.title}
{column.items.map((item) => ( ))} {column.items.length === 0 && (

No documents

)}
))}
); } function WorkspaceDetails(props: { workspace?: Workspace; document?: DocumentRecord; }) { return ( ); } function UsersView(props: { users: User[]; selectedUser?: User; onSelectUser: (id: string) => void; onCreateUser: (input: { email: string; displayName: string; role: Role; }) => Promise; }) { return (
Users
{props.users.map((user) => ( ))}
); } function UserDetails(props: { selectedUser?: User; onCreateUser: (input: { email: string; displayName: string; role: Role; }) => Promise; }) { const [email, setEmail] = useState(""); const [displayName, setDisplayName] = useState(""); const [role, setRole] = useState("viewer"); async function submit(event: Event) { event.preventDefault(); await props.onCreateUser({ email, displayName, role }); setEmail(""); setDisplayName(""); setRole("viewer"); } return ( ); } function buildDocumentColumns( documents: DocumentRecord[], activePath: string, ): BrowserColumn[] { const activeSegments = activePath.split("/").filter(Boolean); const prefixes = [""]; const maxFolderDepth = activePath.endsWith(".md") ? activeSegments.length - 1 : activeSegments.length; for (let index = 0; index < maxFolderDepth; index += 1) { prefixes.push(activeSegments.slice(0, index + 1).join("/")); } return prefixes.map((prefix) => ({ title: prefix ? folderLabel(prefix) : "Documents", prefix, items: collectItems(documents, prefix), })); } function collectItems( documents: DocumentRecord[], prefix: string, ): BrowserItem[] { const items = new Map(); const prefixWithSlash = prefix ? `${prefix}/` : ""; for (const document of documents) { if (!document.path.startsWith(prefixWithSlash)) { continue; } const remainder = document.path.slice(prefixWithSlash.length); if (!remainder || remainder === "index.md") { continue; } const [nextSegment, ...rest] = remainder.split("/"); if (rest.length > 0) { const folderPath = `${prefixWithSlash}${nextSegment}`; const indexDocument = documents.find( (candidate) => candidate.path === `${folderPath}/index.md`, ); items.set(folderPath, { type: "folder", name: folderLabel(nextSegment), path: folderPath, indexDocument, }); continue; } items.set(document.path, { type: "document", name: document.title || fileLabel(document.path), path: document.path, document, }); } return [...items.values()].sort((left, right) => { if (left.type !== right.type) { return left.type === "folder" ? -1 : 1; } return left.name.localeCompare(right.name); }); } function selectDocument(documents: DocumentRecord[], activePath: string) { if (!activePath) { return documents[0]; } return ( documents.find((document) => document.path === activePath) ?? documents.find((document) => document.path === `${activePath}/index.md`) ); } function isKnownDocumentLocation(documents: DocumentRecord[], path: string) { if (!path) { return false; } return documents.some( (document) => document.path === path || document.path === `${path}/index.md` || document.path.startsWith(`${path}/`), ); } function isActiveItem(item: BrowserItem, activePath: string) { return ( activePath === item.path || activePath === item.indexDocument?.path || activePath.startsWith(`${item.path}/`) ); } function documentHref(path: string) { return `/docs/${path.replace(/(^|\/)index\.md$/, "").replace(/\.md$/, "")}`; } function folderLabel(path: string) { return fileLabel(path.split("/").filter(Boolean).at(-1) ?? path); } function fileLabel(path: string) { return path .replace(/\.md$/, "") .replace(/[-_]/g, " ") .replace(/\b\w/g, (match) => match.toUpperCase()); } async function api(path: string, init?: RequestInit): Promise { const response = await fetch(path, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}), }, }); if (!response.ok) { const text = await response.text(); throw new Error(text || response.statusText); } return response.json() as Promise; } function readSession(): Session | null { try { const raw = localStorage.getItem(sessionKey); return raw ? (JSON.parse(raw) as Session) : null; } catch { return null; } } function formatDate(value: string) { if (!value) { return ""; } return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", }).format(new Date(value)); } function formatTime(value: string) { return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }).format(new Date(value)); }