import { useCallback, useEffect, useId, useMemo, useRef, useState, type FormEvent, } from 'react'; import { Badge, Button, Dialog, DialogTitle, Input, Textarea, } from '@noorddev/vlak-react'; type Status = 'Needs review' | 'Approved' | 'Changes requested'; type WorkItem = { id: string; title: string; project: string; image: string; alt: string; status: Status; feedback: string; }; type StoredState = { statuses: Record; feedback: Record; }; const STORAGE_KEY = 'review-room-v1'; const INITIAL_ITEMS: WorkItem[] = [ { id: '01', title: 'Field notes', project: 'Polder', image: '/assets/01.jpg', alt: 'Laundry beside a brick house overlooking fields.', status: 'Needs review', feedback: '', }, { id: '02', title: 'A place to pause', project: 'Polder', image: '/assets/02.jpg', alt: 'Two chairs and a thermos beside the water.', status: 'Needs review', feedback: '', }, { id: '03', title: 'Through the dike', project: 'Noord', image: '/assets/03.jpg', alt: 'A cyclist emerging from a concrete passage through a dike.', status: 'Approved', feedback: '', }, { id: '04', title: 'Outside Café De Polder', project: 'Polder', image: '/assets/04.jpg', alt: 'A person and a dog outside Café De Polder.', status: 'Changes requested', feedback: 'Leave more room around the entrance.', }, { id: '05', title: 'The waiting place at the edge of the field', project: 'Noord', image: '/assets/05.jpg', alt: 'A roadside shelter in an open Dutch landscape.', status: 'Needs review', feedback: '', }, { id: '06', title: 'Walking home', project: 'Noord', image: '/assets/06.jpg', alt: 'A person and a dog on a path beside open fields.', status: 'Approved', feedback: '', }, ]; const FILTERS = ['All', 'Needs review', 'Approved', 'Changes requested'] as const; type Filter = (typeof FILTERS)[number]; function isStatus(value: unknown): value is Status { return ( value === 'Needs review' || value === 'Approved' || value === 'Changes requested' ); } function loadStored(): StoredState { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { return { statuses: {}, feedback: {} }; } const parsed = JSON.parse(raw) as unknown; if (!parsed || typeof parsed !== 'object') { return { statuses: {}, feedback: {} }; } const record = parsed as Record; const statuses: Record = {}; const feedback: Record = {}; const statusSource = record.statuses && typeof record.statuses === 'object' ? (record.statuses as Record) : {}; const feedbackSource = record.feedback && typeof record.feedback === 'object' ? (record.feedback as Record) : {}; for (const item of INITIAL_ITEMS) { if (isStatus(statusSource[item.id])) { statuses[item.id] = statusSource[item.id]; } if (typeof feedbackSource[item.id] === 'string') { feedback[item.id] = feedbackSource[item.id]; } } return { statuses, feedback }; } catch { return { statuses: {}, feedback: {} }; } } function badgeVariant(status: Status): 'outline' | 'solid' | 'muted' { if (status === 'Approved') return 'solid'; if (status === 'Changes requested') return 'outline'; return 'muted'; } export default function App() { const searchId = useId(); const feedbackId = useId(); const errorId = useId(); const [stored, setStored] = useState(() => loadStored()); const [search, setSearch] = useState(''); const [filter, setFilter] = useState('All'); const [openId, setOpenId] = useState(null); const [draftFeedback, setDraftFeedback] = useState(''); const [feedbackError, setFeedbackError] = useState(null); const openerRef = useRef(null); const reviewButtonsRef = useRef>(new Map()); const items = useMemo( () => INITIAL_ITEMS.map((item) => ({ ...item, status: stored.statuses[item.id] ?? item.status, feedback: stored.feedback[item.id] ?? item.feedback, })), [stored], ); useEffect(() => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); } catch { // Ignore quota / private-mode failures. } }, [stored]); const counts = useMemo(() => { const next = { All: items.length, 'Needs review': 0, Approved: 0, 'Changes requested': 0, }; for (const item of items) { next[item.status] += 1; } return next; }, [items]); const visible = useMemo(() => { const q = search.trim().toLowerCase(); return items.filter((item) => { const matchesFilter = filter === 'All' || item.status === filter; const matchesSearch = q.length === 0 || item.title.toLowerCase().includes(q) || item.project.toLowerCase().includes(q); return matchesFilter && matchesSearch; }); }, [items, filter, search]); const openItem = openId ? items.find((item) => item.id === openId) ?? null : null; const persistItem = useCallback((id: string, status: Status, feedback: string) => { setStored((prev) => ({ statuses: { ...prev.statuses, [id]: status }, feedback: { ...prev.feedback, [id]: feedback }, })); }, []); const openReview = (id: string) => { const item = items.find((entry) => entry.id === id); if (!item) return; openerRef.current = reviewButtonsRef.current.get(id) ?? null; setOpenId(id); setDraftFeedback(item.feedback); setFeedbackError(null); }; const closeReview = () => { setOpenId(null); setFeedbackError(null); const opener = openerRef.current; window.requestAnimationFrame(() => { opener?.focus(); }); }; const clearFilters = () => { setSearch(''); setFilter('All'); }; const approve = () => { if (!openItem) return; const nextFeedback = draftFeedback; persistItem(openItem.id, 'Approved', nextFeedback); setFeedbackError(null); }; const requestChanges = (event?: FormEvent) => { event?.preventDefault(); if (!openItem) return; const trimmed = draftFeedback.trim(); if (!trimmed) { setFeedbackError('Add a specific request before asking for changes.'); return; } persistItem(openItem.id, 'Changes requested', trimmed); setDraftFeedback(trimmed); setFeedbackError(null); }; const needsDecision = counts['Needs review'] + counts['Changes requested']; return (

Review room

Six photographs to decide together

See what still needs a decision, inspect an image, then approve it or leave a specific request for changes.

Still open {needsDecision}
setSearch(event.target.value)} placeholder="Search by title or project" autoComplete="off" />
{FILTERS.map((name) => { const selected = filter === name; return (
{counts[name]} items
); })}
{visible.length === 0 ? (

No matching work

Nothing matches this search and filter. Clear them to see all six photographs again.

) : (
    {visible.map((item) => (
  • {item.alt}

    {item.project}

    {item.status}

    {item.title}

    {item.feedback ? (

    {item.feedback}

    ) : null}
  • ))}
)} {openItem ? (
Review {openItem.title}
{openItem.alt}

{openItem.project}

{openItem.title}

{openItem.status} {openItem.feedback ? (

Saved feedback {openItem.feedback}

) : (

No feedback saved yet.

)}