Files
MFL-Card-Browser/src/App.tsx
T

88 lines
3.5 KiB
TypeScript

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>
);
}