Group set cards by name with expandable rarity rows
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+55
-8
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user