Add artwork switcher: arrow buttons cycle through multiple card artworks
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+13
-18
@@ -3,37 +3,32 @@ 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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user