feat(tcg): add burn UI to collection, build passes
- CollectionView: burn mode with 5-card selection (commons only) - Burn bar with confirmation, reward cookies credited - Selected cards highlighted with fire emoji - Non-common cards dimmed in burn mode - a11y: role/tabIndex/onKeyDown for keyboard navigation - Vite build: EXIT_CODE=0 (max-old-space-size=3072) - All 250 tests pass (106 backend + 46 protocol + 98 frontend)
This commit is contained in:
@@ -142,6 +142,10 @@ function CollectionView() {
|
||||
const [rarity, setRarity] = useState<TcgRarity | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [burnMode, setBurnMode] = useState(false);
|
||||
const [burning, setBurning] = useState(false);
|
||||
const [burnResult, setBurnResult] = useState<{ burned: number; reward: number; balance: number } | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -160,6 +164,38 @@ function CollectionView() {
|
||||
load().catch(() => undefined);
|
||||
}, [load]);
|
||||
|
||||
const toggleSelect = (instanceId: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(instanceId)) {
|
||||
next.delete(instanceId);
|
||||
} else if (next.size < 5) {
|
||||
next.add(instanceId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const doBurn = async () => {
|
||||
if (selected.size !== 5) return;
|
||||
setBurning(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await controller.burnCards([...selected]);
|
||||
setBurnResult({ burned: result.burned.length, reward: result.reward, balance: result.balance });
|
||||
setSelected(new Set());
|
||||
setBurnMode(false);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
setBurning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCount = selected.size;
|
||||
const canBurn = selectedCount === 5;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="botsu-tcg-filters">
|
||||
@@ -182,7 +218,33 @@ function CollectionView() {
|
||||
{RARITY_LABELS[r]}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="botsu-tcg-filter-btn"
|
||||
data-active={burnMode}
|
||||
onClick={() => { setBurnMode(!burnMode); setSelected(new Set()); }}
|
||||
>
|
||||
Brûler
|
||||
</button>
|
||||
</div>
|
||||
{burnMode && (
|
||||
<div className="botsu-tcg-burn-bar">
|
||||
<span>Sélectionne 5 cartes communes à brûler ({selectedCount}/5)</span>
|
||||
<button
|
||||
type="button"
|
||||
className="botsu-tcg-booster-btn"
|
||||
onClick={doBurn}
|
||||
disabled={!canBurn || burning}
|
||||
>
|
||||
{burning ? '…' : 'Confirmer le brûlage'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{burnResult && (
|
||||
<div className="botsu-tcg-burn-result">
|
||||
{burnResult.burned} cartes brûlées. Récompense: {burnResult.reward} cookies. Solde: {burnResult.balance}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="botsu-tcg-error">{error}</div>}
|
||||
{loading && <div className="botsu-tcg-loading">Chargement…</div>}
|
||||
{page && !loading && (
|
||||
@@ -192,7 +254,21 @@ function CollectionView() {
|
||||
) : (
|
||||
<div className="botsu-tcg-cards">
|
||||
{page.cards.map((card) => (
|
||||
<CardView key={card.instanceId} card={card} />
|
||||
<div
|
||||
key={card.instanceId}
|
||||
data-selected={burnMode && selected.has(card.instanceId)}
|
||||
role={burnMode && card.rarity === 'common' ? 'button' : undefined}
|
||||
tabIndex={burnMode && card.rarity === 'common' ? 0 : undefined}
|
||||
onClick={burnMode && card.rarity === 'common' ? () => toggleSelect(card.instanceId) : undefined}
|
||||
onKeyDown={
|
||||
burnMode && card.rarity === 'common'
|
||||
? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSelect(card.instanceId); } }
|
||||
: undefined
|
||||
}
|
||||
style={burnMode && card.rarity !== 'common' ? { opacity: 0.3, pointerEvents: 'none' } : undefined}
|
||||
>
|
||||
<CardView card={card} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -329,4 +329,50 @@
|
||||
border-radius: var(--botsu-radius);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Burn mode */
|
||||
.botsu-tcg-burn-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem 1rem;
|
||||
background: color-mix(in srgb, #e55 10%, var(--botsu-color-surface));
|
||||
border: 1px solid color-mix(in srgb, #e55 40%, transparent);
|
||||
border-radius: var(--botsu-radius);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.botsu-tcg-burn-bar span {
|
||||
font-size: 0.85rem;
|
||||
color: var(--botsu-color-text-muted);
|
||||
}
|
||||
|
||||
.botsu-tcg-burn-bar button {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.botsu-tcg-burn-result {
|
||||
padding: 0.6rem 1rem;
|
||||
background: color-mix(in srgb, #4a9 10%, var(--botsu-color-surface));
|
||||
border: 1px solid color-mix(in srgb, #4a9 40%, transparent);
|
||||
border-radius: var(--botsu-radius);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.botsu-tcg-cards > [data-selected="true"] .botsu-tcg-card {
|
||||
border-color: #e55;
|
||||
box-shadow: 0 0 8px color-mix(in srgb, #e55 30%, transparent);
|
||||
}
|
||||
|
||||
.botsu-tcg-cards > [data-selected="true"]::before {
|
||||
content: '🔥';
|
||||
position: absolute;
|
||||
margin-left: -0.3rem;
|
||||
margin-top: -0.3rem;
|
||||
font-size: 1rem;
|
||||
z-index: 1;
|
||||
}
|
||||
Reference in New Issue
Block a user