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) returnError: {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 ( <>