import { useEffect, useMemo, useRef, useState } from 'react'; import { Badge, Button, Dialog, DialogTitle, Input, Textarea } from '@noorddev/vlak-react'; type Status = 'Needs review' | 'Approved' | 'Changes requested'; type Filter = 'All' | Status; type Item = { id: string; title: string; project: string; image: string; alt: string; status: Status; feedback: string; }; type SavedEntry = { status: Status; feedback: string }; type Saved = Record; const STORAGE_KEY = 'review-room-v1'; const STATUSES: Status[] = ['Needs review', 'Approved', 'Changes requested']; const FILTERS: Filter[] = ['All', ...STATUSES]; const ITEMS: Item[] = [ { 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 BADGE_VARIANT: Record = { 'Needs review': 'solid', 'Changes requested': 'outline', Approved: 'muted', }; function isStatus(value: unknown): value is Status { return typeof value === 'string' && (STATUSES as string[]).includes(value); } function loadSaved(): Saved { try { if (typeof window === 'undefined') return {}; const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return {}; const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}; const record = parsed as Record; const saved: Saved = {}; for (const item of ITEMS) { const entry = record[item.id]; if (!entry || typeof entry !== 'object') continue; const { status, feedback } = entry as { status?: unknown; feedback?: unknown }; saved[item.id] = { status: isStatus(status) ? status : item.status, feedback: typeof feedback === 'string' ? feedback : item.feedback, }; } return saved; } catch { return {}; } } function initialItems(): Item[] { const saved = loadSaved(); return ITEMS.map((item) => (saved[item.id] ? { ...item, ...saved[item.id] } : item)); } export default function App() { const [items, setItems] = useState(initialItems); const [query, setQuery] = useState(''); const [filter, setFilter] = useState('All'); const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(''); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const openerRef = useRef(null); const resultsRef = useRef(null); useEffect(() => { try { const payload: Saved = {}; for (const item of items) payload[item.id] = { status: item.status, feedback: item.feedback }; window.localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch { // Storage is unavailable; the session still works in memory. } }, [items]); // Return focus to the opener once the dialog has closed. useEffect(() => { if (selectedId !== null) return; const opener = openerRef.current; openerRef.current = null; const target = opener && opener.isConnected ? opener : resultsRef.current; if (target) requestAnimationFrame(() => target.focus()); }, [selectedId]); const counts = useMemo(() => { const result: Record = { All: items.length, 'Needs review': 0, Approved: 0, 'Changes requested': 0 }; for (const item of items) result[item.status] += 1; return result; }, [items]); const visible = useMemo(() => { const needle = query.trim().toLowerCase(); return items.filter((item) => { const matchesStatus = filter === 'All' || item.status === filter; const matchesQuery = needle === '' || item.title.toLowerCase().includes(needle) || item.project.toLowerCase().includes(needle); return matchesStatus && matchesQuery; }); }, [items, query, filter]); const selected = selectedId === null ? null : items.find((item) => item.id === selectedId) ?? null; const pending = counts['Needs review']; const isFiltered = query.trim() !== '' || filter !== 'All'; function openReview(item: Item, opener: HTMLElement) { openerRef.current = opener; setDraft(item.feedback); setError(''); setNotice(''); setSelectedId(item.id); } function closeReview() { setSelectedId(null); } function updateItem(id: string, patch: Partial) { setItems((current) => current.map((item) => (item.id === id ? { ...item, ...patch } : item))); } function approve() { if (!selected) return; updateItem(selected.id, { status: 'Approved', feedback: draft.trim() }); setError(''); setNotice('Approved. Close the review when you are ready.'); } function requestChanges() { if (!selected) return; const text = draft.trim(); if (text === '') { setError('Describe what should change before requesting changes.'); setNotice(''); return; } updateItem(selected.id, { status: 'Changes requested', feedback: text }); setError(''); setNotice('Changes requested. Your feedback is saved with this image.'); } function clearFilters() { setQuery(''); setFilter('All'); } return (

Review room

Six photographs shared with your project partner. Approve what is ready, or say exactly what should change.

{pending === 0 ? 'Every image has a decision.' : `${pending} of ${items.length} still ${pending === 1 ? 'needs' : 'need'} a decision.`}

setQuery(event.currentTarget.value)} autoComplete="off" />
{FILTERS.map((value) => ( ))}

{filter === 'All' ? 'All work' : filter} {visible.length === items.length ? `${items.length} images` : `${visible.length} of ${items.length} images`}

{visible.length === 0 ? (

Nothing matches

{query.trim() !== '' ? `No titles or projects match “${query.trim()}”${filter === 'All' ? '' : ` in ${filter.toLowerCase()}`}.` : `There is nothing in ${filter.toLowerCase()} right now.`}

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

    {item.title}

    {item.feedback !== '' && (

    Feedback {item.feedback}

    )}
  • ))}
)} {isFiltered && visible.length > 0 && (

Showing a filtered view.{' '}

)}
{selected && (
{selected.alt}

{selected.project}

{selected.title}
Status
{selected.status}
Saved feedback
{selected.feedback !== '' ? (
{selected.feedback}
) : ( None yet )}
{ event.preventDefault(); requestChanges(); }} >