import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../auth'; import { getCollections, createCollection, deleteCollection as apiDeleteCollection, updateCollection } from '../api'; import ShareDialog from '../components/ShareDialog'; interface Collection { id: string; name: string; description: string; is_public: number; created_by: string; board_count: number; member_count: number; created_at: string; updated_at: string; member_role: string | null; preview_thumbnail: string | null; preview_thumbnails: string | null; } function hashCode(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return Math.abs(hash); } function timeAgo(dateStr: string): string { const d = new Date(dateStr); const now = new Date(); const diff = now.getTime() - d.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days}d ago`; return d.toLocaleDateString(); } // Seeded pseudo-random for consistent offsets per card function seededRandom(seed: number) { let s = seed; return () => { s = (s * 16807 + 0) % 2147483647; return (s - 1) / 2147483646; }; } // Three-dot dropdown menu for card actions function CardMenu({ onRename, onShare, onDelete, isOwner }: { onRename: () => void; onShare?: () => void; onDelete?: () => void; isOwner: boolean; }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [open]); return (
{open && (
e.stopPropagation()}> {[ { label: 'Rename', onClick: onRename, show: true }, { label: 'Share', onClick: onShare, show: isOwner && !!onShare }, { label: 'Delete', onClick: onDelete, show: isOwner && !!onDelete, danger: true }, ].filter(i => i.show).map((item, idx) => ( ))}
)}
); } export default function CollectionList() { const [collections, setCollections] = useState([]); const [search, setSearch] = useState(''); const [showModal, setShowModal] = useState(false); const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); const [creating, setCreating] = useState(false); const [shareColId, setShareColId] = useState(null); const [renamingId, setRenamingId] = useState(null); const [renameText, setRenameText] = useState(''); const renameRef = useRef(null); const navigate = useNavigate(); const { user, logout } = useAuth(); const load = useCallback(async (q?: string) => { try { const res = await getCollections(q); setCollections(res.data.collections || []); } catch (err) { console.error('Failed to load collections:', err); } }, []); useEffect(() => { load(); }, [load]); useEffect(() => { const timer = setTimeout(() => load(search || undefined), 300); return () => clearTimeout(timer); }, [search, load]); useEffect(() => { if (renamingId) renameRef.current?.select(); }, [renamingId]); async function handleCreate() { if (!newName.trim()) return; setCreating(true); try { const res = await createCollection(newName.trim(), newDesc.trim()); const col = res.data.collection || res.data; setShowModal(false); setNewName(''); setNewDesc(''); navigate(`/collection/${col.id}`); } catch (err) { console.error('Failed to create collection:', err); } finally { setCreating(false); } } async function handleDelete(id: string) { if (!confirm('Delete this collection and ALL its boards? This cannot be undone.')) return; try { await apiDeleteCollection(id); setCollections((prev) => prev.filter((c) => c.id !== id)); } catch (err) { console.error('Failed to delete collection:', err); } } async function commitRename(id: string) { const trimmed = renameText.trim(); const col = collections.find(c => c.id === id); if (trimmed && trimmed !== col?.name) { try { await updateCollection(id, { name: trimmed }); setCollections((prev) => prev.map((c) => c.id === id ? { ...c, name: trimmed } : c)); } catch (err) { console.error('Failed to rename:', err); } } setRenamingId(null); } function getThumbnails(col: Collection): string[] { if (!col.preview_thumbnails) return []; return col.preview_thumbnails.split('|||').filter(Boolean).slice(0, 4); } // Stacked photo offsets — consistent per collection via seeded random function getStackOffsets(id: string, count: number) { const rng = seededRandom(hashCode(id)); return Array.from({ length: count }, () => ({ rotate: (rng() - 0.5) * 16, // -8 to +8 degrees offsetX: (rng() - 0.5) * 20, // -10 to +10 px offsetY: (rng() - 0.5) * 12, // -6 to +6 px })); } return (
{/* Header */}
RefBoard
{(user?.display_name || user?.email || '?')[0].toUpperCase()}
{user?.display_name || user?.email}
{/* Title + Controls */}

Collections

{collections.length} collection{collections.length !== 1 ? 's' : ''}
setSearch(e.target.value)} style={{ width: '100%', padding: '8px 14px 8px 34px', background: '#141414', border: '1px solid #222', borderRadius: '8px', color: '#e0e0e0', fontSize: '13px', outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.15s', }} onFocus={(e) => { e.currentTarget.style.borderColor = '#333'; }} onBlur={(e) => { e.currentTarget.style.borderColor = '#222'; }} />
{/* Grid */}
{collections.length === 0 && (
No collections yet
Create a collection to organize your reference boards
)} {collections.map((col) => { const thumbs = getThumbnails(col); const offsets = getStackOffsets(col.id, thumbs.length); const isOwner = col.created_by === user?.id; return (
navigate(`/collection/${col.id}`)} style={{ background: '#131313', borderRadius: '12px', border: '1px solid #1e1e1e', cursor: 'pointer', transition: 'all 0.2s ease', position: 'relative', }} onMouseEnter={(e) => { const el = e.currentTarget as HTMLElement; el.style.borderColor = '#2a2a2a'; el.style.transform = 'translateY(-3px)'; el.style.boxShadow = '0 12px 32px rgba(0,0,0,0.35)'; }} onMouseLeave={(e) => { const el = e.currentTarget as HTMLElement; el.style.borderColor = '#1e1e1e'; el.style.transform = 'none'; el.style.boxShadow = 'none'; }} > {/* Preview — stacked board thumbnails like scattered photos */}
{thumbs.length > 0 ? ( // Stacked/fanned thumbnails thumbs.map((t, i) => { const o = offsets[i]; const z = thumbs.length - i; // first on top return (
); }) ) : (
{col.board_count === 0 ? 'Empty collection' : `${col.board_count} board${col.board_count !== 1 ? 's' : ''}`}
)}
{/* Name + menu */}
{renamingId === col.id ? ( setRenameText(e.target.value)} onBlur={() => commitRename(col.id)} onKeyDown={(e) => { if (e.key === 'Enter') commitRename(col.id); if (e.key === 'Escape') setRenamingId(null); e.stopPropagation(); }} onClick={(e) => e.stopPropagation()} style={{ width: '100%', fontSize: '14px', fontWeight: 600, color: '#e0e0e0', background: '#0a0a0a', border: '1px solid #4a9eff', borderRadius: '4px', padding: '2px 6px', outline: 'none', boxSizing: 'border-box', }} autoFocus /> ) : (

{col.name}

)} {col.description && (

{col.description}

)}
{ setRenameText(col.name); setRenamingId(col.id); }} onShare={() => setShareColId(col.id)} onDelete={() => handleDelete(col.id)} isOwner={isOwner} />
{/* Footer */}
{col.board_count} board{col.board_count !== 1 ? 's' : ''} | {col.is_public ? ( Public ) : ( {col.member_count} member{col.member_count !== 1 ? 's' : ''} )} | {timeAgo(col.updated_at)}
); })}
{/* Create modal */} {showModal && (
setShowModal(false)}>
e.stopPropagation()}>

New Collection

setNewName(e.target.value)} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); }} style={{ width: '100%', padding: '10px 14px', marginBottom: '10px', background: '#0a0a0a', border: '1px solid #2a2a2a', borderRadius: '8px', color: '#e0e0e0', fontSize: '14px', outline: 'none', boxSizing: 'border-box', }} onFocus={(e) => { e.currentTarget.style.borderColor = '#4a9eff'; }} onBlur={(e) => { e.currentTarget.style.borderColor = '#2a2a2a'; }} /> setNewDesc(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); }} style={{ width: '100%', padding: '10px 14px', marginBottom: '20px', background: '#0a0a0a', border: '1px solid #2a2a2a', borderRadius: '8px', color: '#e0e0e0', fontSize: '14px', outline: 'none', boxSizing: 'border-box', }} onFocus={(e) => { e.currentTarget.style.borderColor = '#4a9eff'; }} onBlur={(e) => { e.currentTarget.style.borderColor = '#2a2a2a'; }} />
)} {shareColId && ( { setShareColId(null); load(); }} /> )}
); }