// ============ TaskQuest — App root (Supabase-backed) ============ const { useState, useEffect, useRef } = React; const { ROLES: RA, PROJECTS: PA, CONSULTANTS: CA, COLUMNS: COLA } = window.TQ; const BE = window.TQ_BACKEND; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "tileLayout": "comfortable", "dmLayout": "cards", "accent": "#5E6AD2" }/*EDITMODE-END*/; let _nid = 500, _bid = 50; function genId(p) { _nid += 1; return p + _nid + Math.random().toString(36).slice(2, 5); } function BootScreen({ label }) { return (
{label || "Loading your workspace…"}
); } // Manager visibility: team members on the current project with no active tasks. function UnassignedBanner({ project, tasks }) { const { ROLES, CONSULTANTS, projectsOf } = window.TQ; const inScope = CONSULTANTS.filter(c => { const pr = projectsOf(ROLES[c]); return project === "all" ? pr.length > 0 : pr.indexOf(project) >= 0; }); const hasActive = id => tasks.some(t => t.assignee === id && t.status !== "done" && (project === "all" || t.project === project)); const unassigned = inScope.filter(c => !hasActive(c)); if (inScope.length === 0 || unassigned.length === 0) return null; return (
{unassigned.length} team member{unassigned.length > 1 ? "s have" : " has"} no active tasks
{unassigned.map(c => ( {ROLES[c].short} ))}
); } // Board employee filter bar (PM/DM) — compact searchable dropdown function BoardFilterBar({ project, filterAssignee, setFilterAssignee }) { const { CONSULTANTS, projectsOf, ROLES } = window.TQ; const pool = CONSULTANTS.filter(c => { const pr = projectsOf(ROLES[c]); return project === "all" ? pr.length > 0 : pr.indexOf(project) >= 0; }); if (pool.length < 2) return null; const ALL = "__all__"; return (
Member
setFilterAssignee(v === ALL ? "" : v)} placeholder="All members" emptyText="No matching members" getLabel={c => c === ALL ? "All members" : (ROLES[c] && ROLES[c].name) || c} renderOption={c => c === ALL ? All members : ({ROLES[c].name}{ROLES[c].label}) } />
); } function App({ authUser, onLogout, theme, setTheme }) { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [role, setRoleRaw] = useState(authUser); const [project, setProject] = useState(RA[authUser] && RA[authUser].type === "dm" ? "all" : "alpha"); const [tasks, setTasks] = useState([]); const [bonus, setBonus] = useState([]); const [notifs, setNotifs] = useState([]); const [deliverables, setDeliverables] = useState([]); const [delivId, setDelivId] = useState(null); const [createDelivOpen, setCreateDelivOpen] = useState(false); const [createCtx, setCreateCtx] = useState(null); // {deliverable, project} when breaking a deliverable down const [dmTab, setDmTab] = useState(() => { try { return localStorage.getItem("tq_dm_tab") || "backlog"; } catch(e) { return "backlog"; } }); const [filterAssignee, setFilterAssignee] = useState(""); const [booted, setBooted] = useState(false); const [bootErr, setBootErr] = useState(""); const [detailId, setDetailId] = useState(null); const [moveModal, setMoveModal] = useState(null); // { kind, taskId } const [createOpen, setCreateOpen] = useState(false); const [bonusModal, setBonusModal] = useState(null); // { to, project } const [notifOpen, setNotifOpen] = useState(false); const [toasts, setToasts] = useState([]); const [viewKey, setViewKey] = useState(0); const [dataVersion, setDataVersion] = useState(0); const [view, setView] = useState("board"); // board | timesheet/timesheets | delivery // accent application (theme lives in Root) useEffect(() => { const root = document.documentElement.style; root.setProperty("--accent", t.accent); root.setProperty("--accent-hover", t.accent); root.setProperty("--accent-soft", t.accent + "26"); }, [t.accent]); useEffect(() => { setViewKey(k => k + 1); }, [project]); // re-render when the timesheet store changes (badges, review queue, card logger) useEffect(() => window.TQ_TIME.subscribe(() => setDataVersion(v => v + 1)), []); // ---------- initial load + realtime ---------- async function reloadTasks() { setTasks(await BE.loadTasks()); } async function reloadDeliverables() { setDeliverables(await BE.loadDeliverables()); } async function reloadBonus() { setBonus(await BE.loadBonus()); } async function reloadNotifs() { setNotifs(await BE.loadNotifications()); } async function reloadProfiles() { const m = await BE.loadProfiles(); if (m) { window.TQ.applyProfiles(m); setDataVersion(v => v + 1); } } async function reloadProjects() { const m = await BE.loadProjects(); if (m) { window.TQ.applyProjects(m); setDataVersion(v => v + 1); } } async function reloadConfig() { const c = await BE.loadConfig(); if (c) { window.TQ.applyConfig(c); setDataVersion(v => v + 1); } } useEffect(() => { let unsub = function () {}; (async () => { try { const profiles = await BE.loadProfiles(); if (profiles) window.TQ.applyProfiles(profiles); const projects = await BE.loadProjects(); if (projects) window.TQ.applyProjects(projects); const cfg = await BE.loadConfig(); if (cfg) window.TQ.applyConfig(cfg); const [tk, dl, bn, nt] = await Promise.all([BE.loadTasks(), BE.loadDeliverables(), BE.loadBonus(), BE.loadNotifications()]); setTasks(tk); setDeliverables(dl); setBonus(bn); setNotifs(nt); // Demo hours are seeded ONLY in offline preview — never against a live workspace. if (!BE.CONFIGURED) { try { window.TQ_TIME.ensureSeed(tk); } catch (e) {} } // pick a sensible default project for this user const me = RA[authUser]; if (me && me.type === "dm") setProject("all"); else if (!PA[project]) { const first = Object.keys(PA)[0]; if (first) setProject(first); } setBooted(true); unsub = BE.subscribe({ reloadTasks, reloadDeliverables, reloadBonus, reloadNotifs, reloadProfiles, reloadProjects, reloadConfig }); } catch (err) { console.error("[TaskQuest] load failed", err); setBootErr(err.message || "Could not load your workspace."); setBooted(true); } })(); return () => { try { unsub(); } catch (e) {} }; }, []); // ---------- persistence helper ---------- function persist(fn) { Promise.resolve().then(fn).catch(err => { console.error("[TaskQuest] save failed", err); addToast("Save failed — change not stored", Ic.Alert, "#eb5757"); }); } function setRole(next) { setRoleRaw(next); setViewKey(k => k + 1); setView("board"); const nr = RA[next]; if (!nr) return; if (nr.type === "dm") setProject("all"); else if (nr.type === "pm" || nr.type === "consultant") setProject(window.TQ.projectsOf(nr)[0] || Object.keys(PA)[0] || "alpha"); else if (project === "all") setProject(Object.keys(PA)[0] || "alpha"); } // ---------- gates ---------- if (!booted) return ; if (bootErr) return ; const r = RA[role] || RA[authUser]; if (!r) return ; const isManager = r.type === "pm" || r.type === "dm"; const canSwitchRole = !BE.CONFIGURED || r.type === "admin"; // prototype tool: locked in prod (admins may impersonate) const detailTask = detailId ? tasks.find(x => x.id === detailId) : null; const moveTask = moveModal ? tasks.find(x => x.id === moveModal.taskId) : null; const detailDeliv = delivId ? deliverables.find(x => x.id === delivId) : null; // ---------- helpers ---------- function addToast(text, icon, color) { const id = genId("t"); setToasts(ts => [...ts, { id, text, icon, color: color || "var(--accent)" }]); setTimeout(() => setToasts(ts => ts.filter(x => x.id !== id)), 3200); } function pushNotif(n) { const full = { id: genId("n"), read: false, time: "just now", ...n }; setNotifs(ns => [full, ...ns]); persist(() => BE.insertNotif(full)); } function updateTask(id, fn) { setTasks(ts => { const next = ts.map(x => x.id === id ? fn(x) : x); const changed = next.find(x => x.id === id); if (changed) persist(() => BE.saveTask(changed)); return next; }); } function addComment(id, author, text, system) { updateTask(id, x => ({ ...x, comments: [...(x.comments || []), { author, text, time: "just now", system: !!system }] })); } // ---------- move / drag ---------- function onMove(task, toCol) { const kind = window.needsModal(task.status, toCol); if (!kind) { updateTask(task.id, x => ({ ...x, status: toCol })); const col = COLA.find(c => c.key === toCol); addComment(task.id, role, `Moved to ${col.name}.`, true); addToast(`Moved to ${col.name}`, Ic.Arrow, col.color); return; } setMoveModal({ kind, taskId: task.id }); } function submitMove(data) { const task = moveTask; if (!task) return; if (moveModal.kind === "date") { updateTask(task.id, x => ({ ...x, status: "unhappy", request: { newDate: data.newDate, reason: data.reason || "—", comments: data.comments, status: "pending" } })); addComment(task.id, role, `Requested a date change to ${window.TQ.fmtDate(data.newDate)}.${data.comments ? " " + data.comments : ""}`); pushNotif({ type: "date_request", who: role, project: task.project, forRoles: ["pm", "dm"], text: `${RA[task.assignee].name} requested a date change on ${task.name}` }); addToast("Date change submitted for approval", Ic.Calendar, "#f2c94c"); } else if (moveModal.kind === "hold") { updateTask(task.id, x => ({ ...x, status: "onhold", request: { reason: data.reason, comments: data.comments, status: "hold" } })); addComment(task.id, role, `Moved to On Hold — ${data.reason}.${data.comments ? " " + data.comments : ""}`); pushNotif({ type: "comment", who: role, project: task.project, forRoles: ["pm", "dm"], text: `${RA[task.assignee].name} put ${task.name} on hold — ${data.reason}` }); addToast("Task moved to On Hold", Ic.Pause, "#878b94"); } else if (moveModal.kind === "done") { updateTask(task.id, x => ({ ...x, status: "done", completion: { onTime: data.onTime, status: "pending", comments: data.comments || "Marked complete." } })); addComment(task.id, role, `Marked done${data.onTime ? " on time" : " (late)"}.${data.comments ? " " + data.comments : ""}`); pushNotif({ type: "done", who: role, project: task.project, forRoles: ["pm", "dm"], text: `${RA[task.assignee].name} marked ${task.name} as done — needs approval` }); addToast(data.onTime ? "Completed on time! Pending sign-off" : "Marked done — pending sign-off", data.onTime ? Ic.Star : Ic.Check, data.onTime ? "#4cb782" : "#878b94"); } setMoveModal(null); } // ---------- approvals ---------- function approveDate(task, approvedDate) { updateTask(task.id, x => ({ ...x, due: approvedDate, request: { ...x.request, status: "approved", approvedDate } })); addComment(task.id, role, `Approved the date change. New due date: ${window.TQ.fmtDateLong(approvedDate)}. Move it back to Happy Path to resume.`, true); pushNotif({ type: "date_approved", who: role, project: task.project, forRoles: [task.assignee], text: `Your date change on ${task.name} was approved — new date ${window.TQ.fmtDate(approvedDate)}` }); addToast("Date change approved", Ic.CheckCircle, "#4cb782"); } function rejectDate(task) { updateTask(task.id, x => ({ ...x, request: { ...x.request, status: "rejected" } })); addComment(task.id, role, `Rejected the date change. Please keep the original due date.`, true); pushNotif({ type: "rejected", who: role, project: task.project, forRoles: [task.assignee], text: `Your date change on ${task.name} was rejected` }); addToast("Date change rejected", Ic.X, "#eb5757"); } function approveDone(task) { updateTask(task.id, x => ({ ...x, completion: { ...x.completion, status: "approved" } })); addComment(task.id, role, `Approved the completion.${task.completion.onTime ? " +10 points awarded." : ""}`, true); pushNotif({ type: "approved", who: role, project: task.project, forRoles: [task.assignee], text: `Your completion of ${task.name} was approved${task.completion.onTime ? " — +10 points" : ""}` }); addToast(task.completion.onTime ? "Completion approved · +10 points" : "Completion approved", Ic.CheckCircle, "#4cb782"); } // ---------- delete task (manager removes a task that is no longer needed) ---------- async function removeTask(id) { setTasks(ts => ts.filter(t => t.id !== id)); try { await BE.deleteTask(id); } catch (err) { console.error(err); } } // ---------- reassign (manager moves a task to a different team member) ---------- function reassignTask(task, newAssignee) { if (!newAssignee || newAssignee === task.assignee) return; const prev = task.assignee; updateTask(task.id, x => ({ ...x, assignee: newAssignee })); addComment(task.id, role, `Reassigned from ${RA[prev] ? RA[prev].name : prev} to ${RA[newAssignee] ? RA[newAssignee].name : newAssignee}.`, true); pushNotif({ type: "comment", who: role, project: task.project, forRoles: [newAssignee], text: `${RA[role].name} reassigned ${task.name} to you` }); addToast("Task reassigned", Ic.Users, "#5E6AD2"); } // ---------- comment ---------- function onComment(task, text) { addComment(task.id, role, text); if (r.type === "consultant") { pushNotif({ type: "comment", who: role, project: task.project, forRoles: ["pm", "dm"], text: `${RA[role].name} added a progress comment on ${task.name}` }); } } // ---------- create ---------- function createTask(data) { const nt = window.TQ.mkTask({ name: data.name, project: data.project, assignee: data.assignee, due: data.due, status: "happy", deliverable: data.deliverable || null, description: data.description || "", subcategory: data.subcategory || null, comments: data.comments ? [{ author: role, text: data.comments, time: "just now" }] : [] }); setTasks(ts => [nt, ...ts]); persist(() => BE.insertTask(nt)); pushNotif({ type: "comment", who: role, project: data.project, forRoles: [data.assignee], text: `${RA[role].name} assigned you a new task: ${data.name}` }); if (nt.deliverable) { const dl = deliverables.find(d => d.id === nt.deliverable); addDeliverableComment(nt.deliverable, role, `Added task “${data.name}” → ${RA[data.assignee] ? RA[data.assignee].name : "the team"}.`, true); if (dl) pushNotif({ type: "comment", who: role, project: dl.project, forRoles: [dl.createdBy || "dm", "dm"], text: `${RA[role].name} broke down ${dl.name} into a new task` }); } addToast("Task created", Ic.Plus, "#5E6AD2"); setCreateOpen(false); setCreateCtx(null); } // ---------- deliverables (DM creates & tracks; PM breaks down) ---------- function addDeliverableComment(id, author, text, system) { setDeliverables(ds => { const next = ds.map(x => x.id === id ? { ...x, comments: [...(x.comments || []), { author, text, time: "just now", system: !!system }] } : x); const ch = next.find(x => x.id === id); if (ch) persist(() => BE.saveDeliverable(ch)); return next; }); } function onDeliverableComment(d, text) { addDeliverableComment(d.id, role, text); } function createDeliverable(data) { const nd = window.TQ.mkDeliverable({ name: data.name, project: data.project, pm: data.pm, due: data.due, priority: data.priority, desc: data.desc, createdBy: role }); setDeliverables(ds => [nd, ...ds]); persist(() => BE.insertDeliverable(nd)); pushNotif({ type: "comment", who: role, project: data.project, forRoles: [data.pm], text: `${RA[role].name} assigned you a deliverable: ${data.name}` }); addToast("Deliverable created", Ic.Layers, "var(--accent)"); setCreateDelivOpen(false); } function acceptDeliverable(d) { setDeliverables(ds => { const next = ds.map(x => x.id === d.id ? { ...x, accepted: true } : x); const ch = next.find(x => x.id === d.id); if (ch) persist(() => BE.saveDeliverable(ch)); return next; }); addDeliverableComment(d.id, role, "Accepted the delivery — signed off and closed.", true); pushNotif({ type: "approved", who: role, project: d.project, forRoles: [d.pm], text: `${RA[role].name} accepted your delivery of ${d.name}` }); addToast("Delivery accepted", Ic.CheckCircle, "#8b7cf6"); } function openCreateForDeliverable(d) { setCreateCtx({ deliverable: d.id, project: d.project }); setCreateOpen(true); } // ---------- bonus ---------- function awardBonus(data) { const b = { id: genId("b"), to: data.to, pts: data.pts, by: role, comment: data.comment, project: data.project, time: "just now" }; setBonus(bs => [b, ...bs]); persist(() => BE.insertBonus(b)); pushNotif({ type: "bonus", who: role, project: data.project, forRoles: [data.to], text: `${RA[role].name} awarded you +${data.pts} points — \u201c${data.comment}\u201d` }); addToast(`Awarded +${data.pts} to ${RA[data.to].short}`, Ic.Sparkle, "#e0902b"); setBonusModal(null); } // ---------- notifications ---------- const visibleNotifs = notifs.filter(n => window.TQ.notifVisible(n, role, r.type)); function markAll() { const ids = visibleNotifs.filter(n => !n.read).map(n => n.id); setNotifs(ns => ns.map(n => window.TQ.notifVisible(n, role, r.type) ? { ...n, read: true } : n)); persist(() => BE.markNotifsRead(ids)); } function clickNotif(n) { setNotifs(ns => ns.map(x => x.id === n.id ? { ...x, read: true } : x)); persist(() => BE.markNotifsRead([n.id])); setNotifOpen(false); } // ---------- admin: users, projects, points config ---------- async function createUser(data) { try { await BE.provisionUser(data); await reloadProfiles(); setDataVersion(v => v + 1); addToast(`Login created for ${data.name}`, Ic.Users, "#5E6AD2"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not create the account", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not create the account." }; } } async function createProject(data) { try { const key = await BE.insertProject(data); await reloadProjects(); setDataVersion(v => v + 1); addToast(`Created ${(PA[key] && PA[key].name) || data.name}`, Ic.Folder, "#16a394"); } catch (err) { console.error(err); addToast("Could not create project", Ic.Alert, "#eb5757"); } } async function createProjects(list) { try { const keys = await BE.insertProjects(list); await reloadProjects(); setDataVersion(v => v + 1); const n = (keys && keys.length) || list.length; addToast(`Created ${n} project${n === 1 ? "" : "s"}`, Ic.Folder, "#16a394"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not create projects", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not create the projects." }; } } async function editProject(key, data) { try { await BE.updateProject(key, data); await reloadProjects(); setDataVersion(v => v + 1); addToast("Project updated", Ic.Folder, "#16a394"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not update project", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not update the project." }; } } async function removeProject(key) { try { await BE.deleteProject(key); await reloadProjects(); await reloadTasks(); setDataVersion(v => v + 1); addToast("Project removed", Ic.Check, "#16a394"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not remove project", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not remove the project." }; } } async function updateUser(data) { try { await BE.updateProfile(data.id, { name: data.name, type: data.type, username: data.username, projects: data.projects }); if (data.password) await BE.resetPassword(data.id, data.password); await reloadProfiles(); setDataVersion(v => v + 1); addToast(`Updated ${data.name}`, Ic.Check, "#16a394"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not update the profile", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not update the profile." }; } } async function deleteUser(id) { try { await BE.deleteUser(id); await reloadProfiles(); await reloadTasks(); setDataVersion(v => v + 1); addToast("User removed", Ic.Check, "#16a394"); return { ok: true }; } catch (err) { console.error(err); addToast("Could not remove the user", Ic.Alert, "#eb5757"); return { ok: false, message: (err && err.message) || "Could not remove the user." }; } } function saveConfig(cfg) { window.TQ.applyConfig(cfg); setDataVersion(v => v + 1); persist(() => BE.saveConfig(cfg)); addToast("Points system updated", Ic.Target, "#e0902b"); } // page heading const heading = r.type === "consultant" ? "My Board" : r.type === "pm" ? "Project Board" : r.type === "admin" ? "System Administration" : "Delivery Backlog"; const subline = r.type === "consultant" ? "Your assigned tasks, points and rank" : r.type === "pm" ? "Break down deliverables, track the team and approve work" : r.type === "admin" ? "Manage members, projects and the points system" : "Assign deliverables to project managers and track delivery"; const tileLayout = t.tileLayout === "comfortable" ? "" : t.tileLayout; // ---------- view tabs (Board / Timesheet) ---------- const tsWeek = window.TQ_TIME.thisWeekStart(); let tsBadge = 0; if (isManager) { const scopeCons = CA.filter(c => (r.type === "dm" && project === "all") ? true : window.TQ.projectsOf(RA[c]).indexOf(project) >= 0); tsBadge = scopeCons.filter(c => window.TQ_TIME.statusOf(c, tsWeek).status === "submitted").length; } const tabs = r.type === "consultant" ? [{ key: "board", label: "My Board", icon: Ic.Layers }, { key: "timesheet", label: "Timesheet", icon: Ic.Clock }] : r.type === "pm" ? [{ key: "board", label: "Project Board", icon: Ic.Layers }, { key: "timesheets", label: "Timesheets", icon: Ic.Clock, count: tsBadge }] : r.type === "dm" ? [{ key: "delivery", label: "Delivery", icon: Ic.Target }, { key: "timesheets", label: "Timesheets", icon: Ic.Clock, count: tsBadge }] : null; const curView = (tabs && tabs.some(x => x.key === view)) ? view : (tabs ? tabs[0].key : "board"); const onTsView = curView === "timesheet" || curView === "timesheets"; return ( <>
setTheme(th => th === "dark" ? "light" : "dark")} authUser={authUser} onLogout={onLogout} canSwitchRole={canSwitchRole} notifications={visibleNotifs} onOpenNotif={() => setNotifOpen(o => !o)} notifOpen={notifOpen} notifPanel={ setNotifOpen(false)} onMarkAll={markAll} onClickNotif={clickNotif} />} />
{heading}
{subline}
{r.type === "dm" && !onTsView && (
)} {tabs && (
{tabs.map(tb => ( ))}
)}
{r.type === "admin" ? ( ) : r.type === "dm" ? ( onTsView ? ( ) : ( setDelivId(d.id)} /> ) ) : onTsView ? ( r.type === "consultant" ? ( ) : ( ) ) : ( <> {r.type === "pm" && ( setDelivId(d.id)} onAddTask={openCreateForDeliverable} /> )} {r.type === "pm" && } {/* Employee filter — PM/DM only */} {(r.type === "pm" || r.type === "dm") && ( )} setDetailId(task.id)} onMove={onMove} /> )}
{/* FAB — DM creates deliverables; PM creates tasks */} {r.type === "dm" && !onTsView && ( )} {r.type === "pm" && !onTsView && ( )} {/* Modals */} {detailTask && ( setDetailId(null)} onComment={onComment} onApproveDate={approveDate} onRejectDate={rejectDate} onApproveDone={approveDone} onReassign={reassignTask} onDelete={(id) => { removeTask(id); setDetailId(null); addToast("Task deleted", Ic.Trash, "var(--red, #e05252)"); }} onEdit={(id, fields) => { updateTask(id, t => ({ ...t, ...fields })); addToast("Task updated", Ic.Check, "#4cb782"); }} onAwardBonus={(to, proj) => { setDetailId(null); setBonusModal({ to, project: proj }); }} /> )} {moveModal && moveTask && ( setMoveModal(null)} onSubmit={submitMove} /> )} {createOpen && ( d.pm === role)} presetDeliverable={createCtx ? createCtx.deliverable : null} onClose={() => { setCreateOpen(false); setCreateCtx(null); }} onCreate={createTask} /> )} {createDelivOpen && ( setCreateDelivOpen(false)} onCreate={createDeliverable} /> )} {detailDeliv && ( setDelivId(null)} onComment={onDeliverableComment} onAccept={(d) => { acceptDeliverable(d); }} onAddTask={(d) => { setDelivId(null); openCreateForDeliverable(d); }} onOpenTask={(tk) => { setDelivId(null); setDetailId(tk.id); }} /> )} {bonusModal && ( setBonusModal(null)} onAward={awardBonus} /> )} setTweak("dmLayout", v)} /> setTweak("tileLayout", v)} /> setTweak("accent", v)} /> ); } function RecognitionActivity({ bonus, project, onAward }) { const scoped = project === "all" ? bonus : bonus.filter(b => b.project === project); return (
Recognition activity
{scoped.length === 0 &&
No recognition yet — award the first bonus.
} {scoped.map((b, i) => (
{RA[b.to] ? RA[b.to].name : "—"} · {b.comment}
{b.time}{b.project && PA[b.project] ? " · " + PA[b.project].name : ""} · by {RA[b.by] ? RA[b.by].short : "—"}
+{b.pts}
))}
); } function Root() { const [theme, setTheme] = useState(() => localStorage.getItem("tq_theme") || "dark"); const [session, setSession] = useState(undefined); // undefined = checking, null = logged out useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); useEffect(() => { localStorage.setItem("tq_theme", theme); }, [theme]); useEffect(() => { let alive = true; BE.getSession().then(s => { if (alive) setSession(s); }); const unsub = BE.onAuthChange(s => { if (alive) setSession(s); }); return () => { alive = false; try { unsub(); } catch (e) {} }; }, []); async function login(role) { const s = role ? { id: role } : await BE.getSession(); setSession(s); } async function logout() { try { await BE.signOut(); } catch (e) {} setSession(null); } if (session === undefined) return ; if (!session) return setTheme(th => th === "dark" ? "light" : "dark")} onLogin={login} />; return ; } ReactDOM.createRoot(document.getElementById("root")).render();