Initial commit: Card & Value Explorer (React/Vite) with AE, MFL, SDG icon filtering, floating card layout, and focus overlay
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import type { CardData } from './types';
|
||||
import cardDataRaw from '../card_data.json?raw';
|
||||
import { AE_LABELS, MFL_LABELS, SDG_LABELS } from './data/labels';
|
||||
import IconBar from './components/IconBar';
|
||||
import IconColumn from './components/IconColumn';
|
||||
import CardField from './components/CardField';
|
||||
import CardFocusOverlay from './components/CardFocusOverlay';
|
||||
import './styles/app.css';
|
||||
|
||||
const cards: CardData[] = JSON.parse(cardDataRaw);
|
||||
|
||||
const AE_COUNT = 13;
|
||||
const MFL_COUNT = 12;
|
||||
const SDG_COUNT = 17;
|
||||
|
||||
function matchesAll(card: CardData, selectedAE: Set<number>, selectedMFL: Set<number>, selectedSDG: Set<number>): boolean {
|
||||
for (const v of selectedAE) if (!card.ae.includes(v)) return false;
|
||||
for (const v of selectedMFL) if (!card.mfl.includes(v)) return false;
|
||||
for (const v of selectedSDG) if (!card.sdgs.includes(v)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [selectedAE, setSelectedAE] = useState<Set<number>>(new Set());
|
||||
const [selectedMFL, setSelectedMFL] = useState<Set<number>>(new Set());
|
||||
const [selectedSDG, setSelectedSDG] = useState<Set<number>>(new Set());
|
||||
const [focusedCardId, setFocusedCardId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setFocusedCardId(null);
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
|
||||
const toggleSet = useCallback((value: number, set: Set<number>, setter: (s: Set<number>) => void) => {
|
||||
const next = new Set(set);
|
||||
if (next.has(value)) next.delete(value);
|
||||
else next.add(value);
|
||||
setter(next);
|
||||
setFocusedCardId(null);
|
||||
}, []);
|
||||
|
||||
const filteredCards = useMemo(() => {
|
||||
const total = selectedAE.size + selectedMFL.size + selectedSDG.size;
|
||||
if (total === 0) return [];
|
||||
return cards.filter((c) => matchesAll(c, selectedAE, selectedMFL, selectedSDG));
|
||||
}, [selectedAE, selectedMFL, selectedSDG]);
|
||||
|
||||
const active = { ae: selectedAE.size > 0, mfl: selectedMFL.size > 0, sdg: selectedSDG.size > 0 };
|
||||
|
||||
const handleFocus = useCallback((id: number | null) => {
|
||||
setFocusedCardId(id);
|
||||
}, []);
|
||||
|
||||
const focusedCard = focusedCardId !== null ? cards.find((c) => c.id === focusedCardId) ?? null : null;
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{focusedCard && <CardFocusOverlay card={focusedCard} onClose={() => setFocusedCardId(null)} />}
|
||||
|
||||
<div className="ae-banner">
|
||||
<IconBar set="ae" count={AE_COUNT} labels={AE_LABELS} selected={selectedAE} onToggle={(v) => toggleSet(v, selectedAE, setSelectedAE)} />
|
||||
</div>
|
||||
|
||||
<div className="sdg-banner">
|
||||
<IconBar set="sdg" count={SDG_COUNT} labels={SDG_LABELS} selected={selectedSDG} onToggle={(v) => toggleSet(v, selectedSDG, setSelectedSDG)} />
|
||||
</div>
|
||||
|
||||
<div className="mfl-sidebar">
|
||||
<IconColumn set="mfl" count={MFL_COUNT} labels={MFL_LABELS} selected={selectedMFL} onToggle={(v) => toggleSet(v, selectedMFL, setSelectedMFL)} />
|
||||
</div>
|
||||
|
||||
<div className="center-field">
|
||||
<div className="field-label-top">
|
||||
<span className="label-ae">{active.ae ? 'AE' : ''}</span>
|
||||
<span className="label-mfl">{active.mfl ? 'MFL' : ''}</span>
|
||||
<span className="label-sdg">{active.sdg ? 'SDG' : ''}</span>
|
||||
<span className="label-count">{filteredCards.length} cards</span>
|
||||
</div>
|
||||
<CardField cards={filteredCards} focusedId={focusedCardId} onFocus={handleFocus} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user