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 Work = { id: string; title: string; project: string; image: string; alt: string; status: Status; feedback: string; }; const STORAGE_KEY = "review-room-plan-v2"; const FILTERS: Filter[] = [ "All", "Needs review", "Approved", "Changes requested", ]; const INITIAL_WORK: Work[] = [ { 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 restoreWork(): Work[] { const defaults = INITIAL_WORK.map((item) => ({ ...item })); if (typeof window === "undefined") return defaults; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (raw === null) return defaults; const stored: unknown = JSON.parse(raw); if (!isRecord(stored)) return defaults; return defaults.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 defaults; } } function persistWork(items: Work[]): boolean { try { const saved: Record> = {}; for (const item of items) { saved[item.id] = { status: item.status, feedback: item.feedback, }; } window.localStorage.setItem(STORAGE_KEY, JSON.stringify(saved)); return true; } catch { return false; } } function StatusBadge({ status }: { status: Status }) { return ( {status} ); } export default function App() { const [items, setItems] = useState(restoreWork); const [search, setSearch] = useState(""); const [filter, setFilter] = useState("All"); const [selectedId, setSelectedId] = useState(null); const [reviewOpen, setReviewOpen] = useState(false); const [feedback, setFeedback] = useState(""); const [feedbackInvalid, setFeedbackInvalid] = useState(false); const [message, setMessage] = useState(""); const pendingFocusId = useRef(null); const query = search.trim().toLowerCase(); const approvedCount = items.filter( (item) => item.status === "Approved", ).length; const selectedItem = items.find((item) => item.id === selectedId); const visibleItems = items.filter((item) => { const matchesSearch = item.title.toLowerCase().includes(query) || item.project.toLowerCase().includes(query); const matchesStatus = filter === "All" || item.status === filter; return matchesSearch && matchesStatus; }); useEffect(() => { if (reviewOpen || pendingFocusId.current === null) return; document.getElementById(`review-${pendingFocusId.current}`)?.focus(); pendingFocusId.current = null; }, [reviewOpen, filter]); function openReview(item: Work) { pendingFocusId.current = null; setSelectedId(item.id); setFeedback(item.feedback); setFeedbackInvalid(false); setMessage(""); setReviewOpen(true); } function closeReview() { if (!reviewOpen) return; // Keep the opener available when an action changes its filtered status. if ( selectedItem && filter !== "All" && selectedItem.status !== filter ) { setFilter("All"); } pendingFocusId.current = selectedId; setReviewOpen(false); } function saveReview(status: Status) { if (!selectedItem) return; const trimmedFeedback = feedback.trim(); if (status === "Changes requested" && trimmedFeedback.length === 0) { setFeedbackInvalid(true); setMessage("Enter feedback before requesting changes."); return; } const nextItems = items.map((item) => item.id === selectedItem.id ? { ...item, status, feedback: trimmedFeedback } : item, ); const persisted = persistWork(nextItems); setItems(nextItems); setFeedback(trimmedFeedback); setFeedbackInvalid(false); const result = status === "Approved" ? "Photograph approved." : "Changes requested."; setMessage( persisted ? `${result} ${trimmedFeedback ? "Feedback saved." : "Review saved."}` : `${result} Changes are available for this session, but could not be saved on this device. Try the action again to retry saving.`, ); } function clearFilters() { setSearch(""); setFilter("All"); document.getElementById("search-work")?.focus(); } return (

Review room

Choose a photograph to review.

{approvedCount} of {items.length} approved

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

    {item.title}

    {item.project}

  • ))}
) : (

No photographs match

Try another search or clear the filters to see all six photographs.

)}
{selectedItem && (
{selectedItem.alt}
{selectedItem.title}

{selectedItem.project}

Saved feedback

{selectedItem.feedback || "No feedback saved yet."}