Add artwork switcher: arrow buttons cycle through multiple card artworks
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-15 22:03:13 +02:00
parent 90932964a5
commit afd02a91ed
3 changed files with 52 additions and 30 deletions
+14 -19
View File
@@ -3,38 +3,33 @@ import React, { createContext, useState } from 'react';
export const CardContext = createContext();
export function CardProvider({ children }) {
// ownedAmounts structure:
// { [card_id]: { "[set_id]-[rarity_id]": amount, ... }, ... }
const [ownedAmounts, setOwnedAmounts] = useState({});
const [expandedCardId, setExpandedCardId] = useState(null);
// cardImages[cardId] is an array of base64 strings, one per artwork index
const [cardImages, setCardImages] = useState({});
const updateAmount = (card_id, key, amount) => {
setOwnedAmounts(prev => ({
...prev,
[card_id]: {
...prev[card_id],
[key]: amount
}
[card_id]: { ...prev[card_id], [key]: amount }
}));
};
const setCardImage = (card_id, blob) => {
setCardImages(prev => ({ ...prev, [card_id]: blob }));
const setCardImage = (card_id, index, blob) => {
setCardImages(prev => {
const existing = prev[card_id] ? [...prev[card_id]] : [];
existing[index] = blob;
return { ...prev, [card_id]: existing };
});
};
return (
<CardContext.Provider
value={{
ownedAmounts,
updateAmount,
expandedCardId,
setExpandedCardId,
cardImages,
setCardImage
}}
>
<CardContext.Provider value={{
ownedAmounts, updateAmount,
expandedCardId, setExpandedCardId,
cardImages, setCardImage,
}}>
{children}
</CardContext.Provider>
);
}
}
+33 -9
View File
@@ -32,6 +32,7 @@ function HomePage() {
const [sortBy, setSortBy] = useState('name');
const { expandedCardId, setExpandedCardId, cardImages, setCardImage, ownedAmounts } = useContext(CardContext);
const [artworkIndex, setArtworkIndex] = useState(0);
const debouncedSearch = useDebounce(searchTerm, 250);
const loadCards = useCallback(() => fetchCards().then(setCards), []);
@@ -40,12 +41,16 @@ function HomePage() {
loadCards().catch(err => setError(err.message)).finally(() => setLoading(false));
}, [loadCards]);
useEffect(() => { setArtworkIndex(0); }, [expandedCardId]);
useEffect(() => {
if (!expandedCardId || cardImages[expandedCardId]) return;
fetchCardImage(expandedCardId)
.then(image => setCardImage(expandedCardId, image))
if (!expandedCardId || !expandedCard) return;
const imageId = expandedCard.image_ids?.[artworkIndex];
if (!imageId || cardImages[expandedCardId]?.[artworkIndex]) return;
fetchCardImage(expandedCardId, imageId)
.then(image => setCardImage(expandedCardId, artworkIndex, image))
.catch(err => console.error('Failed to load card image', err));
}, [expandedCardId, cardImages, setCardImage]);
}, [expandedCardId, artworkIndex, expandedCard, cardImages, setCardImage]);
const getTotal = useCallback((card) =>
card.printings?.reduce((sum, p) => {
@@ -127,11 +132,30 @@ function HomePage() {
<div style={{ flex: 1, padding: '16px', overflowY: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
{expandedCard ? (
<>
{cardImages[expandedCardId] ? (
<img src={cardImages[expandedCardId]} alt={expandedCard.name} style={{ maxWidth: '100%' }} />
) : (
<div style={{ color: '#444', fontSize: '12px', padding: '2rem' }}>Loading image</div>
)}
<div style={{ position: 'relative', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{cardImages[expandedCardId]?.[artworkIndex] ? (
<img src={cardImages[expandedCardId][artworkIndex]} alt={expandedCard.name} style={{ maxWidth: '100%' }} />
) : (
<div style={{ color: '#444', fontSize: '12px', padding: '2rem' }}>Loading image</div>
)}
{(expandedCard.image_ids?.length ?? 0) > 1 && (
<div style={{ position: 'absolute', bottom: '6px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<button
className="icon-btn"
onClick={() => setArtworkIndex(i => Math.max(0, i - 1))}
disabled={artworkIndex === 0}
></button>
<span style={{ fontSize: '11px', color: '#555' }}>
{artworkIndex + 1} / {expandedCard.image_ids.length}
</span>
<button
className="icon-btn"
onClick={() => setArtworkIndex(i => Math.min(expandedCard.image_ids.length - 1, i + 1))}
disabled={artworkIndex === expandedCard.image_ids.length - 1}
></button>
</div>
)}
</div>
<div style={{ width: '100%', borderTop: '1px solid #222', paddingTop: '10px', display: 'flex', flexDirection: 'column', gap: '5px' }}>
<span style={{ fontSize: '15px', fontWeight: 600, color: '#e0e0e0' }}>{expandedCard.name}</span>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
+5 -2
View File
@@ -7,8 +7,11 @@ export async function fetchCards() {
return await response.json();
}
export async function fetchCardImage(cardId) {
const response = await fetch(`${API_BASE}/cardImage/${cardId}`);
export async function fetchCardImage(cardId, imageId) {
const url = imageId
? `${API_BASE}/cardImage/${cardId}?imageId=${imageId}`
: `${API_BASE}/cardImage/${cardId}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch card image');
const data = await response.json();
return data.image;