import { useCallback, useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import { Badge, Button, Dialog, DialogTitle, Input, Textarea, } from "@noorddev/vlak-react"; const STORAGE_KEY = "planning-room-v2"; const CAPACITY = 16; const STATES = ["This week", "Waiting", "Later", "Done"] as const; const FILTERS = ["All", ...STATES] as const; type TaskState = (typeof STATES)[number]; type Filter = (typeof FILTERS)[number]; type Task = { id: string; title: string; project: string; owner: string; hours: number; due: string | null; state: TaskState; blocker: string; nextAction: string; }; type Draft = Pick; const INITIAL_TASKS: readonly Task[] = [ { id: "p01", title: "Review the annotation flow", project: "Noord", owner: "Sam", hours: 4, due: "2026-09-15", state: "This week", blocker: "", nextAction: "Check keyboard use in the review dialog.", }, { id: "p02", title: "Write the brand guide", project: "Polder", owner: "Alex", hours: 6, due: "2026-09-17", state: "This week", blocker: "", nextAction: "Draft the color and typography sections.", }, { id: "p03", title: "Prepare the partner review", project: "Studio", owner: "Rae", hours: 3, due: "2026-09-16", state: "This week", blocker: "", nextAction: "Choose two directions and write the review questions.", }, { id: "p04", title: "Fix the empty state on small screens", project: "Vlak", owner: "Sam", hours: 5, due: "2026-09-18", state: "This week", blocker: "", nextAction: "Reproduce the overflow at 390px.", }, { id: "p05", title: "Export the campaign images", project: "Polder", owner: "Alex", hours: 2, due: "2026-09-16", state: "Waiting", blocker: "Waiting for approved copy.", nextAction: "Ask for the final headline.", }, { id: "p06", title: "Check payment confirmation copy", project: "Studio", owner: "Rae", hours: 3, due: "2026-09-18", state: "Waiting", blocker: "Waiting for the finance review.", nextAction: "Confirm which receipt details are required.", }, { id: "p07", title: "Document how the long project title behaves in the navigation", project: "Vlak", owner: "Sam", hours: 2, due: null, state: "Later", blocker: "", nextAction: "Capture the narrow and wide examples.", }, { id: "p08", title: "Explore a new photo sequence", project: "Noord", owner: "Alex", hours: 4, due: null, state: "Later", blocker: "", nextAction: "Choose six photographs to compare.", }, { id: "p09", title: "Send the workshop notes", project: "Studio", owner: "Rae", hours: 1, due: "2026-09-14", state: "Done", blocker: "", nextAction: "Notes sent for participant correction.", }, { id: "p10", title: "Check the font licenses", project: "Polder", owner: "Alex", hours: 2, due: "2026-09-14", state: "Done", blocker: "", nextAction: "License links saved with the source files.", }, ]; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isTaskState(value: unknown): value is TaskState { return STATES.some((state) => state === value); } function restoreTasks(): Task[] { const defaults = INITIAL_TASKS.map((task) => ({ ...task })); if (typeof window === "undefined") return defaults; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return defaults; const stored: unknown = JSON.parse(raw); if (!isRecord(stored)) return defaults; return defaults.map((task) => { if (!Object.prototype.hasOwnProperty.call(stored, task.id)) return task; const entry = stored[task.id]; if ( !isRecord(entry) || !isTaskState(entry.state) || typeof entry.nextAction !== "string" || !entry.nextAction.trim() || (task.blocker !== "" && entry.state === "This week") ) { return task; } return { ...task, state: entry.state, nextAction: entry.nextAction.trim(), }; }); } catch { return defaults; } } function persistTasks(tasks: Task[]): boolean { try { const stored: Record> = {}; for (const task of tasks) { stored[task.id] = { state: task.state, nextAction: task.nextAction, }; } window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); return true; } catch { return false; } } function plannedTotal(tasks: Task[]): number { return tasks.reduce( (total, task) => total + (task.state === "This week" ? task.hours : 0), 0, ); } function hoursLabel(hours: number): string { return `${hours} ${hours === 1 ? "hour" : "hours"}`; } function balanceLabel(planned: number): string { return planned > CAPACITY ? `${hoursLabel(planned - CAPACITY)} over capacity` : `${hoursLabel(CAPACITY - planned)} remaining`; } const dueFormatter = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC", }); function DueDate({ due }: { due: string | null }) { return due ? ( ) : ( No due date ); } function TaskBadge({ state }: { state: TaskState }) { return ( {state === "Done" && } {state} ); } export default function App() { const [tasks, setTasks] = useState(restoreTasks); const [query, setQuery] = useState(""); const [activeFilter, setActiveFilter] = useState("All"); const [draft, setDraft] = useState(null); const [attemptedSave, setAttemptedSave] = useState(false); const [saveMessage, setSaveMessage] = useState(""); const [storageUnavailable, setStorageUnavailable] = useState(false); const openerRef = useRef(null); const filterGroupRef = useRef(null); const formRef = useRef(null); const stateSelectRef = useRef(null); const returnFocusPending = useRef(false); const planned = plannedTotal(tasks); const excess = Math.max(0, planned - CAPACITY); const remaining = Math.max(0, CAPACITY - planned); const counts: Record = { All: tasks.length, "This week": 0, Waiting: 0, Later: 0, Done: 0, }; for (const task of tasks) counts[task.state] += 1; const search = query.trim().toLowerCase(); const visibleTasks = tasks.filter( (task) => (activeFilter === "All" || task.state === activeFilter) && [task.title, task.project, task.owner].some((value) => value.toLowerCase().includes(search), ), ); const editingTask = draft ? tasks.find((task) => task.id === draft.id) : undefined; const blockedSelection = Boolean( editingTask?.blocker && draft?.state === "This week", ); const noteError = Boolean( draft && attemptedSave && !draft.nextAction.trim(), ); const previewHours = draft && editingTask ? planned - (editingTask.state === "This week" ? editingTask.hours : 0) + (draft.state === "This week" ? editingTask.hours : 0) : planned; const closeEditor = useCallback(() => { returnFocusPending.current = true; setDraft(null); setAttemptedSave(false); setSaveMessage(""); }, []); useEffect(() => { if (draft !== null || !returnFocusPending.current) return; returnFocusPending.current = false; const opener = openerRef.current; const target = opener?.isConnected && opener.getClientRects().length > 0 ? opener : filterGroupRef.current?.querySelector( 'button[aria-pressed="true"]', ); target?.focus({ preventScroll: true }); openerRef.current = null; }, [draft]); function openEditor(task: Task, opener: HTMLButtonElement) { openerRef.current = opener; returnFocusPending.current = false; setAttemptedSave(false); setSaveMessage(""); setDraft({ id: task.id, state: task.state, nextAction: task.nextAction, }); } function clearFilters() { setQuery(""); setActiveFilter("All"); filterGroupRef.current ?.querySelector('button[aria-label="All"]') ?.focus(); } function saveTask(event: FormEvent) { event.preventDefault(); if (!draft || !editingTask) return; setAttemptedSave(true); setSaveMessage(""); if (editingTask.blocker && draft.state === "This week") { stateSelectRef.current?.focus(); return; } const nextAction = draft.nextAction.trim(); if (!nextAction) { formRef.current?.querySelector("textarea")?.focus(); return; } const updatedTasks = tasks.map((task) => task.id === draft.id ? { ...task, state: draft.state, nextAction } : task, ); const persisted = persistTasks(updatedTasks); setTasks(updatedTasks); setDraft({ ...draft, nextAction }); setAttemptedSave(false); setStorageUnavailable(!persisted); setSaveMessage( persisted ? `Task saved. ${balanceLabel(plannedTotal(updatedTasks))}.` : "Task updated for this visit. Browser storage is unavailable, so changes will be lost on reload.", ); } return ( <> Skip to tasks

September 14–18, 2026

Planning room

Make a manageable plan and keep the next step clear.

0} >

Weekly capacity

Planned
{planned} hours
Capacity
{CAPACITY} hours
{excess > 0 ? "Over capacity" : "Remaining"}
{excess > 0 ? excess : remaining} hours

{excess > 0 && ( )} {balanceLabel(planned)}

{excess > 0 ? `Move at least ${hoursLabel(excess)} from This week to Later to make the plan fit.` : remaining === 0 ? "All 16 hours are planned. Adding more will put the week over capacity." : `You have room for ${hoursLabel(remaining)} of focused work.`}

{counts["This week"]}{" "} {counts["This week"] === 1 ? "task" : "tasks"} planned. {" "} Only This week uses capacity. Waiting, Later, and Done are excluded.

Tasks

The full picture, with the next step in view.

setQuery(event.currentTarget.value)} />
{FILTERS.map((filter) => ( ))}

{visibleTasks.length} of {tasks.length} shown

{visibleTasks.length === 0 ? (

No matching tasks

Try another search or return to the full list.

) : (
    {visibleTasks.map((task) => (
  • Project: {task.project}

    {task.title}

    Next action {task.nextAction}

    {task.blocker && (

    Blocked: {task.blocker}

    )}
    Owner
    {task.owner}
    Estimate
    {hoursLabel(task.hours)}
    Due date
    State
  • ))}
)}
{storageUnavailable ? "Browser storage is unavailable. Changes are kept for this visit." : "Saved states and next actions stay in this browser."}
{draft && editingTask && (

Task details

{editingTask.title}

Choose a state and a clear next action. Save to apply your changes.

Project
{editingTask.project}
Owner
{editingTask.owner}
Estimate
{hoursLabel(editingTask.hours)}
Due date

Blocker

{editingTask.blocker || "No blocker. This task is available to plan."}

{editingTask.blocker && (

This task cannot move to This week while blocked.

)}

Only This week counts toward the 16-hour plan.

{blockedSelection && ( )}