import { useEffect, useRef, useState } from "react"; import { Button, Input, Badge, Dialog, DialogTitle, Textarea, } from "@noorddev/vlak-react"; type Status = "Needs review" | "Approved" | "Changes requested"; type Filter = "All" | Status; type WorkItem = { id: string; title: string; project: string; image: string; alt: string; status: Status; feedback: string; }; const STORAGE_KEY = "review-room-v1"; const FILTERS: Filter[] = [ "All", "Needs review", "Approved", "Changes requested", ]; 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: "", }, ]; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isStatus(value: unknown): value is Status { return ( value === "Needs review" || value === "Approved" || value === "Changes requested" ); } function restoreItems(): WorkItem[] { try { const stored: unknown = JSON.parse( window.localStorage.getItem(STORAGE_KEY) ?? "null", ); if (!isRecord(stored)) return INITIAL_ITEMS; return INITIAL_ITEMS.map((item) => { const saved = stored[item.id]; if (!isRecord(saved)) return item; return { ...item, status: isStatus(saved.status) ? saved.status : item.status, feedback: typeof saved.feedback === "string" ? saved.feedback : item.feedback, }; }); } catch { return INITIAL_ITEMS; } } function StatusBadge({ status }: { status: Status }) { return ( {status} ); } export default function App() { const [items, setItems] = useState(restoreItems); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("All"); const [selectedId, setSelectedId] = useState(null); const [feedback, setFeedback] = useState(""); const [feedbackError, setFeedbackError] = useState(false); const [confirmation, setConfirmation] = useState(""); const [storageError, setStorageError] = useState(false); const openerRef = useRef(null); useEffect(() => { try { const saved = Object.fromEntries( items.map(({ id, status, feedback: savedFeedback }) => [ id, { status, feedback: savedFeedback }, ]), ); window.localStorage.setItem(STORAGE_KEY, JSON.stringify(saved)); setStorageError(false); } catch { setStorageError(true); } }, [items]); const counts: Record = { All: items.length, "Needs review": items.filter((item) => item.status === "Needs review").length, Approved: items.filter((item) => item.status === "Approved").length, "Changes requested": items.filter( (item) => item.status === "Changes requested", ).length, }; const normalizedQuery = query.trim().toLowerCase(); const visibleItems = items.filter( (item) => (filter === "All" || item.status === filter) && (item.title.toLowerCase().includes(normalizedQuery) || item.project.toLowerCase().includes(normalizedQuery)), ); const selectedItem = items.find((item) => item.id === selectedId); function openReview(item: WorkItem, opener: HTMLButtonElement) { openerRef.current = opener; setFeedback(item.feedback); setFeedbackError(false); setConfirmation(""); setSelectedId(item.id); } function closeReview() { if (selectedId === null) return; const opener = openerRef.current; setSelectedId(null); setFeedbackError(false); setConfirmation(""); window.requestAnimationFrame(() => { if (opener?.isConnected) opener.focus(); }); } function saveDecision(status: Status) { if (!selectedItem) return; const trimmedFeedback = feedback.trim(); if (status === "Changes requested" && !trimmedFeedback) { setFeedbackError(true); setConfirmation(""); document.getElementById("review-feedback")?.focus(); return; } setItems((current) => current.map((item) => item.id === selectedItem.id ? { ...item, status, feedback: trimmedFeedback } : item, ), ); setFeedback(trimmedFeedback); setFeedbackError(false); // Preserve the opener when the decision moves this item out of its filter. const changedFilter = filter !== "All" && filter !== status; if (changedFilter) setFilter("All"); setConfirmation( `${status === "Approved" ? "Photograph approved." : "Changes requested. Feedback saved."}${ changedFilter ? " The collection now shows all work." : "" }`, ); } function clearFilters() { setQuery(""); setFilter("All"); document.getElementById("work-search")?.focus(); } return (
Polder Noord Photography selection

A closer look

Review room

Six photographs to consider together. Approve a favorite, or say what needs another look.

{counts.Approved} of {items.length} approved

{counts["Needs review"]} still need review

{FILTERS.map((option) => ( ))}
setQuery(event.target.value)} />

Showing {visibleItems.length} of {items.length} photographs

Collection 01
{visibleItems.length > 0 ? (
    {visibleItems.map((item) => (
  • {item.alt}
    {item.project}

    {item.title}

  • ))}
) : (

No photographs found

Try a different title or project, or clear your filters to see all six photographs.

)}
Polder / Noord

{storageError ? "Your browser couldn’t save changes. They’ll stay available during this visit." : "Decisions and feedback are saved in this browser."}

{selectedItem && (
{selectedItem.alt}

{selectedItem.project} Photograph {selectedItem.id} of 06

{selectedItem.project}

{selectedItem.title}
{selectedItem.feedback && (

Saved feedback

{selectedItem.feedback}

)}