import React, { useEffect, useState, useContext, useMemo, useCallback } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { CardContext } from '../context/CardContext'; import { fetchCards, fetchCardImage } from '../services/api'; import { useDebounce } from '../hooks/useDebounce'; import { useMediaQuery } from '../hooks/useMediaQuery'; import CardRow from '../components/CardRow/CardRow'; import SearchBar from '../components/SearchBar/SearchBar'; import FilterBar from '../components/FilterBar/FilterBar'; import Footer from '../components/Footer/Footer'; import PrintingRow from '../components/PrintingRow/PrintingRow'; const BADGE = { monster: { background: '#3a2000', color: '#d4820a' }, spell: { background: '#002820', color: '#10a06a' }, trap: { background: '#280020', color: '#c040a0' }, other: { background: '#202020', color: '#666' }, }; function typeBadge(type) { const t = (type || '').toLowerCase(); if (t.includes('monster')) return BADGE.monster; if (t.includes('spell')) return BADGE.spell; if (t.includes('trap')) return BADGE.trap; return BADGE.other; } const MOBILE_PRINTING_COLS = '60px 1fr 100px 80px'; const DESKTOP_PRINTING_COLS = '80px 1fr 150px 100px'; function HomePage() { const [cards, setCards] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [typeFilter, setTypeFilter] = useState('All'); const [ownedOnly, setOwnedOnly] = useState(false); const [sortBy, setSortBy] = useState('name'); const { expandedCardId, setExpandedCardId, cardImages, setCardImage, ownedAmounts } = useContext(CardContext); const [artworkIndex, setArtworkIndex] = useState(0); const isMobile = useMediaQuery('(max-width: 768px)'); const sheetRef = React.useRef(null); const handleRef = React.useRef(null); const dragStartY = React.useRef(null); React.useEffect(() => { const handle = handleRef.current; if (!handle) return; const onStart = e => { dragStartY.current = e.touches[0].clientY; }; const onMove = e => { e.preventDefault(); const dy = e.touches[0].clientY - dragStartY.current; if (dy > 0 && sheetRef.current) sheetRef.current.style.transform = `translateY(${dy}px)`; }; const onEnd = e => { const dy = e.changedTouches[0].clientY - dragStartY.current; if (dy > 80) { setExpandedCardId(null); } else if (sheetRef.current) { sheetRef.current.style.transition = 'transform 0.2s ease'; sheetRef.current.style.transform = ''; setTimeout(() => { if (sheetRef.current) sheetRef.current.style.transition = ''; }, 200); } }; handle.addEventListener('touchstart', onStart, { passive: true }); handle.addEventListener('touchmove', onMove, { passive: false }); handle.addEventListener('touchend', onEnd, { passive: true }); return () => { handle.removeEventListener('touchstart', onStart); handle.removeEventListener('touchmove', onMove); handle.removeEventListener('touchend', onEnd); }; }, [expandedCardId, setExpandedCardId]); const debouncedSearch = useDebounce(searchTerm, 250); const loadCards = useCallback(() => fetchCards().then(setCards), []); useEffect(() => { loadCards().catch(err => setError(err.message)).finally(() => setLoading(false)); }, [loadCards]); useEffect(() => { setArtworkIndex(0); }, [expandedCardId]); useEffect(() => { if (!expandedCardId) return; const imageIds = cardImages[expandedCardId]?.ids; const imageId = imageIds?.[artworkIndex]; if (cardImages[expandedCardId]?.blobs[artworkIndex]) return; fetchCardImage(expandedCardId, imageId) .then(({ image, image_ids }) => setCardImage(expandedCardId, artworkIndex, image, image_ids)) .catch(err => console.error('Failed to load card image', err)); }, [expandedCardId, artworkIndex, cardImages, setCardImage]); const getTotal = useCallback((card) => card.printings?.reduce((sum, p) => { const key = `${p.set_id}-${p.rarity_id}`; return sum + (ownedAmounts[card.id]?.[key] ?? p.amount_owned ?? 0); }, 0) ?? 0 , [ownedAmounts]); const expandedCard = cards.find(c => c.id === expandedCardId); const stats = useMemo(() => ({ total: cards.length, uniqueOwned: cards.filter(c => getTotal(c) > 0).length, totalCopies: cards.reduce((s, c) => s + getTotal(c), 0), }), [cards, getTotal]); const filteredCards = useMemo(() => { let result = cards; if (debouncedSearch) { const q = debouncedSearch.toLowerCase(); result = result.filter(c => c.name.toLowerCase().includes(q)); } if (typeFilter !== 'All') { const q = typeFilter.toLowerCase(); result = result.filter(c => c.type.toLowerCase().includes(q)); } if (ownedOnly) result = result.filter(c => getTotal(c) > 0); const sorted = [...result]; if (sortBy === 'owned') sorted.sort((a, b) => getTotal(b) - getTotal(a)); else sorted.sort((a, b) => a.name.localeCompare(b.name)); return sorted; }, [cards, debouncedSearch, typeFilter, ownedOnly, sortBy, getTotal]); if (loading) return

Loading cards…

; if (error) return

Error: {error}

; const cardDetailContent = (card) => { if (!card) return null; const imgs = cardImages[card.id]; const blob = imgs?.blobs[artworkIndex]; const ids = imgs?.ids ?? []; const printingCols = isMobile ? MOBILE_PRINTING_COLS : DESKTOP_PRINTING_COLS; return ( <>
{blob ? {card.name} :
Loading image…
} {ids.length > 1 && (
{artworkIndex + 1} / {ids.length}
)}
{card.name}
{card.type} {card.attribute && {card.attribute}} {card.race && {card.race}}
{card.level != null && {'★'.repeat(Math.min(card.level, 13))} Lv.{card.level}} {card.link_val != null && Link {card.link_val}}
{isMobile && card.printings?.length > 0 && (
CodeSetRarityOwned
{card.printings.map((printing, i) => ( ))}
)} ); }; return (
{/* Stats bar */}
{isMobile ? ( <> {stats.total.toLocaleString()} cards {stats.uniqueOwned.toLocaleString()} owned {stats.totalCopies.toLocaleString()} copies ) : ( <> {stats.total.toLocaleString()} cards in DB {stats.uniqueOwned.toLocaleString()} unique owned {stats.totalCopies.toLocaleString()} total copies )}
{/* Main panels */}
{/* Card list */}
{filteredCards.length.toLocaleString()} cards
} />
{/* Desktop: right panel */} {!isMobile && (
{expandedCard ? cardDetailContent(expandedCard) : Click a card to see details }
)}
{/* Mobile: bottom sheet */} {isMobile && expandedCard && ( <>
setExpandedCardId(null)} />
{cardDetailContent(expandedCard)}
)}
); } export default HomePage;