// index-grid.jsx — horizontal-scrolling board of project columns. // Exposes window.IndexGrid const { useRef: useIRef, useEffect: useIEffect, useState: useIState } = React; function TitleCard({ p }) { const isSuperflux = p.client && p.client.toLowerCase().includes('superflux'); return (
{p.title}
{p.year}
{p.collab && (
{p.collab}
)} {isSuperflux && (
{p.client}
)}
); } function KeywordsCard({ p }) { if (!p.keywords || !p.keywords.length) return null; return (
key words:
{p.keywords.join(', ')}
); } // Mini drag-to-reorder thumbnail grid for per-column preview editing function ThumbGrid({ order, gallery, onReorder }) { const [dragFrom, setDragFrom] = useIState(null); const [dragOver, setDragOver] = useIState(null); const onDrop = (e, idx) => { e.preventDefault(); if (dragFrom === null || dragFrom === idx) { setDragFrom(null); setDragOver(null); return; } const next = [...order]; const [moved] = next.splice(dragFrom, 1); next.splice(idx, 0, moved); onReorder(next); setDragFrom(null); setDragOver(null); }; return (
{order.map((origIdx, dispIdx) => { const item = gallery[origIdx]; const isImg = item && item.type === 'image'; return (
setDragFrom(dispIdx)} onDragOver={(e) => { e.preventDefault(); setDragOver(dispIdx); }} onDragLeave={() => setDragOver(null)} onDrop={(e) => onDrop(e, dispIdx)} onDragEnd={() => { setDragFrom(null); setDragOver(null); }} style={{ width: 44, height: 34, cursor: 'grab', position: 'relative', overflow: 'hidden', borderRadius: 2, flexShrink: 0, opacity: dragFrom === dispIdx ? 0.2 : 1, outline: dragOver === dispIdx && dragFrom !== dispIdx ? '2px solid rgba(255,255,255,0.8)' : '1px solid rgba(255,255,255,0.1)', }}> {isImg ? :
{item && item.type === 'vimeo' ? 'vim' : 'vid'}
}
{dispIdx + 1}
); })}
); } function ProjectColumn({ p, cardMode, imageLimit, locked, onOpen, onHover }) { const orderKey = 'gallery-order-' + p.id; const countKey = 'preview-count-' + p.id; const [order, setOrder] = useIState(() => { try { const s = JSON.parse(localStorage.getItem(orderKey)); if (Array.isArray(s) && s.length === p.gallery.length) return s; } catch (_) {} return p.gallery.map((_, i) => i); }); const [previewCount, setPreviewCount] = useIState(() => { try { const s = parseInt(localStorage.getItem(countKey), 10); if (!isNaN(s) && s > 0) return s; } catch (_) {} return null; // null = fall back to global imageLimit }); const [editing, setEditing] = useIState(false); const effective = Math.min( previewCount !== null ? previewCount : imageLimit, p.gallery.length ); const updateOrder = (next) => { setOrder(next); try { localStorage.setItem(orderKey, JSON.stringify(next)); } catch (_) {} }; const nudgeCount = (delta) => { const next = Math.max(1, Math.min(p.gallery.length, effective + delta)); setPreviewCount(next); try { localStorage.setItem(countKey, String(next)); } catch (_) {} }; const visibleItems = order.slice(0, effective).map((origIdx) => ({ item: p.gallery[origIdx], origIdx, })); const btnBase = { fontFamily: FONT, fontSize: 10, background: 'none', border: '1px solid rgba(255,255,255,0.2)', borderRadius: 3, color: '#fff', cursor: 'pointer', padding: '2px 7px', lineHeight: 1.5, }; return (
!editing && onHover(p)} onMouseLeave={() => !editing && onHover(null)} > {(cardMode === 'title' || cardMode === 'full') && (
onOpen(p)} style={{ cursor: 'pointer' }}>
)} {cardMode === 'full' && } {/* Per-column edit panel (author only, when unlocked) */} {!locked && (
{editing && (
PREVIEW COUNT
{effective} / {p.gallery.length}
ORDER (first {effective} shown)
)}
)} {/* Image column */}
{visibleItems.map(({ item, origIdx }) => (
onOpen(p)} style={{ cursor: 'pointer', lineHeight: 0 }}>
))} {effective < p.gallery.length && (
onOpen(p)} style={{ fontFamily: FONT, fontSize: 11, color: 'rgba(255,255,255,0.32)', padding: '4px 0', cursor: 'pointer', letterSpacing: '0.04em', }}> +{p.gallery.length - effective} more
)}
); } function IndexGrid({ cardMode, imageLimit, locked, onOpen, onHover, isMobile = false, locationStyle }) { const allProjects = window.PROJECTS; const orderKey = 'index-project-order-v1'; const [projectOrder, setProjectOrder] = useIState(() => { try { const saved = JSON.parse(localStorage.getItem(orderKey)); if (Array.isArray(saved) && saved.length === allProjects.length) return saved; } catch (_) {} return allProjects.map((_, i) => i); }); const [dragColFrom, setDragColFrom] = useIState(null); const [dragColOver, setDragColOver] = useIState(null); const scrollRef = useIRef(null); const projects = projectOrder.map((i) => allProjects[i]); const onColDrop = (e, toIdx) => { e.preventDefault(); if (dragColFrom === null || dragColFrom === toIdx) { setDragColFrom(null); setDragColOver(null); return; } const next = [...projectOrder]; const [moved] = next.splice(dragColFrom, 1); next.splice(toIdx, 0, moved); setProjectOrder(next); try { localStorage.setItem(orderKey, JSON.stringify(next)); } catch (_) {} setDragColFrom(null); setDragColOver(null); }; // Wheel → horizontal scroll (eased); mouse drag to pan useIEffect(() => { const el = scrollRef.current; if (!el) return; let targetLeft = el.scrollLeft, animating = false, raf = 0; const tick = () => { const cur = el.scrollLeft; const next = cur + (targetLeft - cur) * 0.14; if (Math.abs(targetLeft - cur) < 0.5) { el.scrollLeft = targetLeft; animating = false; return; } el.scrollLeft = next; raf = requestAnimationFrame(tick); }; const onWheel = (e) => { if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { if (!animating) targetLeft = el.scrollLeft; const max = el.scrollWidth - el.clientWidth; targetLeft = Math.max(0, Math.min(max, targetLeft + e.deltaY)); e.preventDefault(); if (!animating) { animating = true; raf = requestAnimationFrame(tick); } } }; el.addEventListener('wheel', onWheel, { passive: false }); let down = false, startX = 0, startScroll = 0; const onDown = (e) => { if (e.target && e.target.closest && e.target.closest('[data-clickable]')) return; if (e.target && e.target.closest && e.target.closest('[data-col-handle]')) return; down = true; startX = e.clientX; startScroll = el.scrollLeft; animating = false; cancelAnimationFrame(raf); el.style.cursor = 'grabbing'; }; const onMove = (e) => { if (!down) return; el.scrollLeft = startScroll - (e.clientX - startX); targetLeft = el.scrollLeft; }; const onUp = () => { down = false; el.style.cursor = ''; }; el.addEventListener('mousedown', onDown); window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); return () => { el.removeEventListener('wheel', onWheel); el.removeEventListener('mousedown', onDown); window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); cancelAnimationFrame(raf); }; }, []); return (
{isMobile && (
)} {projects.map((p, colIdx) => (
{ e.dataTransfer.effectAllowed = 'move'; setDragColFrom(colIdx); } : undefined} onDragOver={!locked ? (e) => { e.preventDefault(); setDragColOver(colIdx); } : undefined} onDragLeave={!locked ? () => setDragColOver(null) : undefined} onDrop={!locked ? (e) => onColDrop(e, colIdx) : undefined} onDragEnd={!locked ? () => { setDragColFrom(null); setDragColOver(null); } : undefined} style={{ position: 'relative', opacity: dragColFrom === colIdx ? 0.35 : 1, outline: !locked && dragColOver === colIdx && dragColFrom !== null && dragColFrom !== colIdx ? '1px solid rgba(255,255,255,0.5)' : '1px solid transparent', transition: 'opacity 0.15s ease', }}> {!locked && (
⠿⠿⠿
)}
))}
{!locked && (
Unlocked — drag ⠿⠿⠿ handles to reorder columns · ✎ edit preview per column
)}
); } window.IndexGrid = IndexGrid;