import React, { useState, useEffect, useRef, useCallback, memo, } from 'react'; import ArtworkCard from './ArtworkCard'; import './MasonryGallery.css'; // ── Masonry helpers ──────────────────────────────────────────────────────── const ROW_SIZE = 8; const ROW_GAP = 16; function applyMasonry(grid) { if (!grid) return; Array.from(grid.querySelectorAll('.nova-card')).forEach((card) => { const media = card.querySelector('.nova-card-media') || card; let height = media.getBoundingClientRect().height || 200; // Clamp to the computed max-height so the span never over-reserves rows // when CSS max-height kicks in (e.g. portrait images capped to 2×16:9). const cssMaxH = parseFloat(getComputedStyle(media).maxHeight); if (!isNaN(cssMaxH) && cssMaxH > 0 && cssMaxH < height) { height = cssMaxH; } const span = Math.max(1, Math.ceil((height + ROW_GAP) / (ROW_SIZE + ROW_GAP))); card.style.gridRowEnd = `span ${span}`; }); } function waitForImages(el) { return Promise.all( Array.from(el.querySelectorAll('img')).map((img) => img.decode ? img.decode().catch(() => null) : Promise.resolve(), ), ); } // ── Page fetch helpers ───────────────────────────────────────────────────── /** * Fetch the next page of data. * * The response is either: * - JSON { artworks: [...], next_cursor: '...' } when X-Requested-With is * sent and the controller returns JSON (future enhancement) * - HTML page – we parse [data-react-masonry-gallery] from it and read its * data-artworks / data-next-cursor / data-next-page-url attributes. */ async function fetchPageData(url) { const res = await fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const ct = res.headers.get('content-type') || ''; // JSON fast-path (if controller ever returns JSON) if (ct.includes('application/json')) { const json = await res.json(); return { artworks: json.artworks ?? [], nextCursor: json.next_cursor ?? null, nextPageUrl: json.next_page_url ?? null, }; } // HTML: parse and extract mount-container data attributes const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const el = doc.querySelector('[data-react-masonry-gallery]'); if (!el) return { artworks: [], nextCursor: null, nextPageUrl: null }; let artworks = []; try { artworks = JSON.parse(el.dataset.artworks || '[]'); } catch { /* empty */ } return { artworks, nextCursor: el.dataset.nextCursor || null, nextPageUrl: el.dataset.nextPageUrl || null, }; } // ── Skeleton row ────────────────────────────────────────────────────────── function SkeletonCard() { return