import { useEffect, 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 BadgeVariant = 'outline' | 'solid' | 'muted'; interface Item { id: string; title: string; project: string; image: string; alt: string; status: Status; feedback: string; } interface Message { kind: 'success' | 'error'; text: string; } const STORAGE_KEY = 'review-room-plan-v2'; const STATUSES: readonly Status[] = ['Needs review', 'Approved', 'Changes requested']; const FILTERS: readonly Filter[] = ['All', 'Needs review', 'Approved', 'Changes requested']; const BADGE_VARIANTS: Record = { 'Needs review': 'outline', Approved: 'solid', 'Changes requested': 'muted', }; const BASE_ITEMS: readonly 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: '', }, ]; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function isStatus(value: unknown): value is Status { return typeof value === 'string' && (STATUSES as readonly string[]).includes(value); } function loadItems(): Item[] { if (typeof window === 'undefined') { return BASE_ITEMS.map((base) => ({ ...base })); } let stored: unknown = null; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (raw !== null) { stored = JSON.parse(raw); } } catch { stored = null; } const record = isRecord(stored) ? stored : {}; return BASE_ITEMS.map((base) => { const entry = record[base.id]; if (!isRecord(entry)) { return { ...base }; } return { ...base, status: isStatus(entry.status) ? entry.status : base.status, feedback: typeof entry.feedback === 'string' ? entry.feedback : base.feedback, }; }); } function saveItems(items: readonly Item[]): void { if (typeof window === 'undefined') { return; } try { const record: Record = {}; for (const item of items) { record[item.id] = { status: item.status, feedback: item.feedback }; } window.localStorage.setItem(STORAGE_KEY, JSON.stringify(record)); } catch { // Storage can be unavailable or full. The in-memory state still works. } } function StatusBadge({ status }: { status: Status }) { return {status}; } export default function App() { const [items, setItems] = useState(loadItems); const [query, setQuery] = useState(''); const [filter, setFilter] = useState('All'); const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(''); const [message, setMessage] = useState(null); const openerRef = useRef(null); const searchFormRef = useRef(null); useEffect(() => { saveItems(items); }, [items]); useEffect(() => { if (selectedId !== null) { return; } const opener = openerRef.current; if (opener === null) { return; } openerRef.current = null; if (opener.isConnected) { opener.focus(); } else { searchFormRef.current?.querySelector('input')?.focus(); } }, [selectedId]); const normalizedQuery = query.trim().toLowerCase(); const visibleItems = items.filter((item) => { const matchesFilter = filter === 'All' || item.status === filter; const matchesQuery = normalizedQuery === '' || item.title.toLowerCase().includes(normalizedQuery) || item.project.toLowerCase().includes(normalizedQuery); return matchesFilter && matchesQuery; }); const approvedCount = items.filter((item) => item.status === 'Approved').length; const selected = selectedId === null ? undefined : items.find((item) => item.id === selectedId); const hasError = message !== null && message.kind === 'error'; function focusSearch() { searchFormRef.current?.querySelector('input')?.focus(); } function clearFilters() { setQuery(''); setFilter('All'); focusSearch(); } function openReview(item: Item, opener: HTMLElement) { openerRef.current = opener; setDraft(item.feedback); setMessage(null); setSelectedId(item.id); } function closeReview() { setSelectedId(null); } function updateItem(id: string, patch: Partial>) { setItems((previous) => previous.map((item) => (item.id === id ? { ...item, ...patch } : item))); } function approve() { if (selected === undefined) { return; } const feedback = draft.trim(); updateItem(selected.id, { status: 'Approved', feedback }); setMessage({ kind: 'success', text: feedback === '' ? 'Marked as approved.' : 'Marked as approved. Feedback saved.', }); } function requestChanges() { if (selected === undefined) { return; } const feedback = draft.trim(); if (feedback === '') { setMessage({ kind: 'error', text: 'Write feedback before requesting changes.' }); return; } updateItem(selected.id, { status: 'Changes requested', feedback }); setMessage({ kind: 'success', text: 'Changes requested. Feedback saved.' }); } function describeEmptyState(): string { const trimmed = query.trim(); if (trimmed !== '' && filter !== 'All') { return `No photographs match “${trimmed}” with the status “${filter}”.`; } if (trimmed !== '') { return `No photographs match “${trimmed}”.`; } return `No photographs have the status “${filter}”.`; } return (

Review room

Choose a photograph to review.

{approvedCount} of {items.length} approved

event.preventDefault()} > setQuery(event.target.value)} />
{FILTERS.map((option) => ( ))}
{visibleItems.length > 0 ? (
    {visibleItems.map((item) => (
  • {item.alt}

    {item.title}

    {item.project}

  • ))}
) : (

No matching photographs

{describeEmptyState()}

)} {selected !== undefined && (
{selected.alt}
{selected.title}
Project
{selected.project}
Status
Saved feedback
{selected.feedback === '' ? 'No feedback yet.' : selected.feedback}