Group set cards by name with expandable rarity rows
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-16 02:08:46 +02:00
parent 1b91a0cc3c
commit d0817c7476
+55 -8
View File
@@ -54,11 +54,10 @@ function SetCardRow({ card, zebra }) {
return ( return (
<div style={{ <div style={{
display: 'grid', gridTemplateColumns: '1fr 120px 90px', display: 'grid', gridTemplateColumns: '1fr 90px',
gap: '8px', padding: '8px 16px', alignItems: 'center', gap: '8px', padding: '6px 16px 6px 32px', alignItems: 'center',
background: zebra ? '#1e1e1e' : '#161616', borderBottom: '1px solid #1a1a1a', background: zebra ? '#1a1a1a' : '#141414', borderBottom: '1px solid #1a1a1a',
}}> }}>
<span style={{ fontSize: '13px', color: '#d8d8d8' }}>{card.name}</span>
<span style={{ fontSize: '12px', color: '#777' }}>{card.rarity_name}</span> <span style={{ fontSize: '12px', color: '#777' }}>{card.rarity_name}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'flex-end' }}>
<button className="icon-btn" onClick={() => current > 0 && save(current - 1)} disabled={current === 0}></button> <button className="icon-btn" onClick={() => current > 0 && save(current - 1)} disabled={current === 0}></button>
@@ -69,6 +68,38 @@ function SetCardRow({ card, zebra }) {
); );
} }
function CardGroup({ group, isExpanded, onToggle }) {
const { ownedAmounts } = useContext(CardContext);
const total = group.printings.reduce((sum, p) => {
const key = `${p.set_id ?? ''}-${p.rarity_id}`;
return sum + (ownedAmounts[group.id]?.[key] ?? p.amount_owned ?? 0);
}, 0);
return (
<>
<div
onClick={onToggle}
className="card-row-header"
style={{
display: 'grid', gridTemplateColumns: '1fr 90px',
gap: '8px', padding: '8px 16px', alignItems: 'center',
cursor: 'pointer', borderBottom: '1px solid #1a1a1a',
background: isExpanded ? '#1c1c1c' : 'transparent',
}}
>
<span style={{ fontSize: '13px', color: '#d8d8d8' }}>{group.name}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'flex-end' }}>
{total > 0 && <span style={{ fontSize: '12px', color: '#aaa' }}>×{total}</span>}
<span style={{ fontSize: '9px', color: '#444' }}>{isExpanded ? '▲' : '▼'}</span>
</div>
</div>
{isExpanded && group.printings.map((p, i) => (
<SetCardRow key={`${group.id}-${p.rarity_id}`} card={p} zebra={i % 2 === 1} />
))}
</>
);
}
function SetsPage() { function SetsPage() {
const [sets, setSets] = useState([]); const [sets, setSets] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -76,6 +107,7 @@ function SetsPage() {
const [setCards, setSetCards] = useState([]); const [setCards, setSetCards] = useState([]);
const [cardsLoading, setCardsLoading] = useState(false); const [cardsLoading, setCardsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [expandedCardId, setExpandedCardId] = useState(null);
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => { useEffect(() => {
@@ -86,6 +118,7 @@ function SetsPage() {
if (selectedSet?.id === set.id) { setSelectedSet(null); setSetCards([]); return; } if (selectedSet?.id === set.id) { setSelectedSet(null); setSetCards([]); return; }
setSelectedSet(set); setSelectedSet(set);
setSetCards([]); setSetCards([]);
setExpandedCardId(null);
setCardsLoading(true); setCardsLoading(true);
fetchSetCards(set.id).then(setSetCards).catch(console.error).finally(() => setCardsLoading(false)); fetchSetCards(set.id).then(setSetCards).catch(console.error).finally(() => setCardsLoading(false));
}; };
@@ -95,6 +128,15 @@ function SetsPage() {
return sets.filter(s => fuzzyMatch(s.set_name, searchTerm) || fuzzyMatch(s.set_code, searchTerm)); return sets.filter(s => fuzzyMatch(s.set_name, searchTerm) || fuzzyMatch(s.set_code, searchTerm));
}, [sets, searchTerm]); }, [sets, searchTerm]);
const groupedCards = useMemo(() => {
const map = new Map();
for (const card of setCards) {
if (!map.has(card.id)) map.set(card.id, { id: card.id, name: card.name, printings: [] });
map.get(card.id).printings.push(card);
}
return [...map.values()];
}, [setCards]);
const setDetail = selectedSet && ( const setDetail = selectedSet && (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: '12px 16px', borderBottom: '1px solid #2a2a2a', flexShrink: 0 }}> <div style={{ padding: '12px 16px', borderBottom: '1px solid #2a2a2a', flexShrink: 0 }}>
@@ -109,15 +151,20 @@ function SetsPage() {
: ( : (
<div style={{ flex: 1, overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ <div style={{
display: 'grid', gridTemplateColumns: '1fr 120px 90px', display: 'grid', gridTemplateColumns: '1fr 90px',
gap: '8px', padding: '5px 16px', gap: '8px', padding: '5px 16px',
fontSize: '10px', color: '#444', textTransform: 'uppercase', letterSpacing: '0.05em', fontSize: '10px', color: '#444', textTransform: 'uppercase', letterSpacing: '0.05em',
borderBottom: '1px solid #222', borderBottom: '1px solid #222',
}}> }}>
<span>Card</span><span>Rarity</span><span style={{ textAlign: 'right' }}>Owned</span> <span>Card</span><span style={{ textAlign: 'right' }}>Owned</span>
</div> </div>
{setCards.map((card, i) => ( {groupedCards.map(group => (
<SetCardRow key={`${card.id}-${card.rarity_id}`} card={card} zebra={i % 2 === 1} /> <CardGroup
key={group.id}
group={group}
isExpanded={expandedCardId === group.id}
onToggle={() => setExpandedCardId(expandedCardId === group.id ? null : group.id)}
/>
))} ))}
</div> </div>
) )