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:
2026-08-06 03:55:22 +02:00
parent f42134e1d0
commit 31fbf571ee
2 changed files with 123 additions and 1 deletions
+77 -1
View File
@@ -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>
)}
+46
View File
@@ -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;
}