// Cases are loaded from cases.json. Each entry has: // `picturePreview`: image URL shown immediately while the video downloads // `videoPreview`: optional video URL — once it finishes downloading, // it replaces the picture preview and plays on loop // A neutral fallback tile is shown if media fails to load. // A per-page-load cache buster appended to every media URL so the browser // can't serve a stale empty/404 from a previous visit. cases.json itself is // already fetched with no-cache. window.__mediaBust = String(Date.now()); function bust(url) { if (!url) return url; // Leave absolute/external URLs alone — only bust same-origin uploads. if (/^https?:\/\//i.test(url)) return url; const sep = url.indexOf("?") === -1 ? "?" : "&"; return url + sep + "v=" + window.__mediaBust; } window.loadWorks = function loadWorks() { return fetch("cases.json", { cache: "no-cache" }).then((r) => { if (!r.ok) throw new Error("Failed to load cases.json: " + r.status); return r.json(); }); }; // Tag list is no longer hardcoded here — filters are now derived from // the actual tags present in cases.json (see Works in app.jsx). // Real media tile. If the image/video fails to load (e.g. missing upload), // fall back to a neutral swatch tile so the card doesn't render a broken icon. // When `work.videoPreview` is set, the picture is shown first while the video // downloads in the background; once the video is ready it fades in over the // image and loops silently. Otherwise the image alone is shown. function WorkPreview({ work }) { const [imgFailed, setImgFailed] = React.useState(false); const [videoFailed, setVideoFailed] = React.useState(false); const [videoReady, setVideoReady] = React.useState(false); const videoRef = React.useRef(null); // Once the video has buffered enough to play through without stalling, // start it and reveal it over the static image. const handleVideoReady = React.useCallback(() => { const v = videoRef.current; if (!v) return; const p = v.play(); if (p && typeof p.catch === "function") p.catch(() => {}); setVideoReady(true); }, []); if (work.picturePreview && !imgFailed) { const hasVideo = !!work.videoPreview && !videoFailed; return (
{work.desc}