import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth } from '../auth'; import { getCollectionDetail, getCollectionByShareToken, createBoard, deleteBoard as apiDeleteBoard, updateBoard, updateCollection, } from '../api'; import ShareDialog from '../components/ShareDialog'; interface Board { id: string; name: string; description: string; image_count: number; thumbnail: string | null; created_at: string; updated_at: string; } interface CollectionDetailProps { isPublicView?: boolean; } 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(); } function CardMenu({ onRename, onDelete, canEdit, canDelete }: { onRename: () => void; onDelete?: () => void; canEdit: boolean; canDelete: 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]); if (!canEdit && !canDelete) return null; return (
{open && (
e.stopPropagation()}> {canEdit && ( )} {canDelete && onDelete && ( )}
)}
); } export default function CollectionDetail({ isPublicView }: CollectionDetailProps) { const { collectionId, shareToken } = useParams<{ collectionId?: string; shareToken?: string }>(); const navigate = useNavigate(); const { user } = useAuth(); const [collection, setCollection] = useState(null); const [boards, setBoards] = useState([]); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [showModal, setShowModal] = useState(false); const [showShare, setShowShare] = useState(false); const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); const [creating, setCreating] = useState(false); const [editingTitle, setEditingTitle] = useState(false); const [titleText, setTitleText] = useState(''); const [renamingBoardId, setRenamingBoardId] = useState(null); const [renameBoardText, setRenameBoardText] = useState(''); const titleRef = useRef(null); const boardRenameRef = useRef(null); const load = useCallback(async () => { try { let res; if (shareToken) { res = await getCollectionByShareToken(shareToken); setCollection(res.data.collection); setBoards(res.data.boards || []); setMembers([]); } else if (collectionId) { res = await getCollectionDetail(collectionId); setCollection(res.data.collection); setBoards(res.data.boards || []); setMembers(res.data.members || []); } setLoading(false); } catch (err: any) { setError(err.response?.data?.error || 'Failed to load collection'); setLoading(false); } }, [collectionId, shareToken]); useEffect(() => { load(); }, [load]); const resolvedId = collectionId || collection?.id; const isOwner = members.some((m: any) => m.user_id === user?.id && m.role === 'owner'); const isEditor = isOwner || members.some((m: any) => m.user_id === user?.id && (m.role === 'editor' || m.role === 'owner')); useEffect(() => { if (editingTitle) titleRef.current?.select(); }, [editingTitle]); useEffect(() => { if (renamingBoardId) boardRenameRef.current?.select(); }, [renamingBoardId]); async function handleCreateBoard() { if (!newName.trim() || !resolvedId) return; setCreating(true); try { const res = await createBoard(resolvedId, newName.trim(), newDesc.trim()); const board = res.data.board || res.data; setShowModal(false); setNewName(''); setNewDesc(''); navigate(`/board/${board.id}`); } catch (err) { console.error('Failed to create board:', err); } finally { setCreating(false); } } async function handleDeleteBoard(boardId: string) { if (!confirm('Delete this board? This cannot be undone.')) return; try { await apiDeleteBoard(boardId); setBoards((prev) => prev.filter((b) => b.id !== boardId)); } catch (err) { console.error('Failed to delete board:', err); } } async function commitBoardRename(boardId: string) { const trimmed = renameBoardText.trim(); const board = boards.find(b => b.id === boardId); if (trimmed && trimmed !== board?.name) { try { await updateBoard(boardId, { name: trimmed }); setBoards((prev) => prev.map((b) => b.id === boardId ? { ...b, name: trimmed } : b)); } catch (err) { console.error('Failed to rename board:', err); } } setRenamingBoardId(null); } async function handleRenameCollection(name: string) { if (!resolvedId) return; try { await updateCollection(resolvedId, { name }); setCollection((prev: any) => ({ ...prev, name })); } catch (err) { console.error('Failed to rename collection:', err); } } if (loading) { return (
Loading...
); } if (error) { return (
{error}
); } return (
{/* Header */}
/ {editingTitle ? ( setTitleText(e.target.value)} onBlur={() => { const t = titleText.trim(); if (t && t !== collection?.name) handleRenameCollection(t); setEditingTitle(false); }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); if (e.key === 'Escape') setEditingTitle(false); }} style={{ fontSize: '15px', fontWeight: 600, color: '#e0e0e0', background: '#0a0a0a', border: '1px solid #4a9eff', borderRadius: '4px', padding: '2px 8px', outline: 'none', minWidth: '120px', maxWidth: '300px', }} autoFocus /> ) : (
{collection?.name || 'Collection'} {isEditor && ( )}
)}
{user && !isPublicView && ( )}
{/* Controls */}
{isEditor && ( )} {collection?.description && ( {collection.description} )}
{boards.length} board{boards.length !== 1 ? 's' : ''}
{/* Board Grid */}
{boards.length === 0 && (
No boards yet
Create a board to start collecting references
)} {boards.map((board) => (
navigate(`/board/${board.id}`)} style={{ background: '#131313', borderRadius: '10px', border: '1px solid #1c1c1c', 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(-2px)'; el.style.boxShadow = '0 8px 24px rgba(0,0,0,0.3)'; }} onMouseLeave={(e) => { const el = e.currentTarget as HTMLElement; el.style.borderColor = '#1c1c1c'; el.style.transform = 'none'; el.style.boxShadow = 'none'; }} > {/* Preview — auto-generated from board content */}
{board.thumbnail ? ( ) : (
{board.image_count > 0 ? `${board.image_count} image${board.image_count !== 1 ? 's' : ''}` : 'Empty board'}
)}
{/* Board name + menu */}
{renamingBoardId === board.id ? ( setRenameBoardText(e.target.value)} onBlur={() => commitBoardRename(board.id)} onKeyDown={(e) => { if (e.key === 'Enter') commitBoardRename(board.id); if (e.key === 'Escape') setRenamingBoardId(null); e.stopPropagation(); }} onClick={(e) => e.stopPropagation()} style={{ width: '100%', fontSize: '13px', fontWeight: 600, color: '#e0e0e0', background: '#0a0a0a', border: '1px solid #4a9eff', borderRadius: '4px', padding: '2px 6px', outline: 'none', boxSizing: 'border-box', }} autoFocus /> ) : (

{board.name}

)}
{ setRenameBoardText(board.name); setRenamingBoardId(board.id); }} onDelete={() => handleDeleteBoard(board.id)} canEdit={isEditor} canDelete={isOwner} />
{/* Footer */}
{board.image_count} img{board.image_count !== 1 ? 's' : ''} | {timeAgo(board.updated_at)}
))}
{/* Create board modal */} {showModal && (
setShowModal(false)}>
e.stopPropagation()}>

New Board

setNewName(e.target.value)} autoFocus onKeyDown={(e) => { if (e.key === 'Enter') handleCreateBoard(); }} 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') handleCreateBoard(); }} 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'; }} />
)} {showShare && resolvedId && ( setShowShare(false)} /> )}
); }