feat: raccourcis clavier, URL partageable, sparkline, recherche identité
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Raccourcis clavier : ←/→ (frames), Espace (play/pause), Échap (quitter animation/fermer info), H (basculer heatmap↔flux) - URL partageable : ?period=7&view=flow&city=Paris — état restauré au chargement et mis à jour sans rechargement (history.replaceState) - Sparkline : mini bar-chart SVG dans le StatsPanel montrant l'activité sur la période (données déjà en mémoire, aucune requête) - Recherche identité : champ flottant (⌕) acceptant un nom Ğ1 ou une clé g1…, résout via Subsquid + Cesium+, bascule en vue flux et met la ville en focus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+44
-3
@@ -4,6 +4,7 @@ import { PeriodSelector } from './components/PeriodSelector';
|
||||
import { HeatMap } from './components/HeatMap';
|
||||
import { FlowMap } from './components/FlowMap';
|
||||
import { AnimationPlayer } from './components/AnimationPlayer';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { fetchData } from './services/DataService';
|
||||
import type { PeriodStats } from './services/DataService';
|
||||
import type { Transaction } from './data/mockData';
|
||||
@@ -13,9 +14,10 @@ import { computeFlowStats } from './data/arcData';
|
||||
import { useAnimation } from './hooks/useAnimation';
|
||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||
import { InfoPanel } from './components/InfoPanel';
|
||||
import { initialUrlState, useUrlSync } from './hooks/useUrlState';
|
||||
|
||||
export default function App() {
|
||||
const [periodDays, setPeriodDays] = useState(7);
|
||||
const [periodDays, setPeriodDays] = useState(initialUrlState.period);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [arcs, setArcs] = useState<TransactionArc[]>([]);
|
||||
const [stats, setStats] = useState<PeriodStats | null>(null);
|
||||
@@ -25,14 +27,17 @@ export default function App() {
|
||||
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
||||
const [currentUD, setCurrentUD] = useState<number>(11.78);
|
||||
const [allTimestamps, setAllTimestamps] = useState<number[]>([]);
|
||||
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap');
|
||||
const [focusCity, setFocusCity] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>(initialUrlState.view);
|
||||
const [focusCity, setFocusCity] = useState<string | null>(initialUrlState.city);
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const isMobile = useMediaQuery('(max-width: 639px)');
|
||||
|
||||
const animation = useAnimation(transactions, arcs, periodDays, allTimestamps);
|
||||
|
||||
// Synchronise l'état dans l'URL (deep link / partage)
|
||||
useUrlSync(periodDays, viewMode, focusCity);
|
||||
|
||||
const handlePeriodChange = (days: number) => {
|
||||
animation.deactivate();
|
||||
setPeriodDays(days);
|
||||
@@ -43,6 +48,31 @@ export default function App() {
|
||||
setFocusCity(null);
|
||||
};
|
||||
|
||||
// Raccourcis clavier
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
if (e.key === 'ArrowLeft' && animation.active) {
|
||||
animation.seek(Math.max(0, animation.currentIndex - 1));
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowRight' && animation.active) {
|
||||
animation.seek(Math.min(animation.frames.length - 1, animation.currentIndex + 1));
|
||||
e.preventDefault();
|
||||
} else if (e.key === ' ' && animation.active) {
|
||||
animation.playing ? animation.pause() : animation.play();
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'Escape') {
|
||||
if (infoOpen) { setInfoOpen(false); e.preventDefault(); }
|
||||
else if (animation.active) { animation.deactivate(); e.preventDefault(); }
|
||||
} else if (e.key === 'h' || e.key === 'H') {
|
||||
handleViewModeChange(viewMode === 'heatmap' ? 'flow' : 'heatmap');
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [animation.active, animation.playing, animation.currentIndex, animation.frames.length, infoOpen, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -101,6 +131,7 @@ export default function App() {
|
||||
viewMode,
|
||||
flowStats,
|
||||
focusCity,
|
||||
allTimestamps,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -140,6 +171,16 @@ export default function App() {
|
||||
ℹ
|
||||
</button>
|
||||
|
||||
{/* Barre de recherche identité */}
|
||||
<div className={`absolute ${isMobile ? 'top-28' : 'top-16'} left-4 z-[1001]`}>
|
||||
<SearchBar
|
||||
onResult={(city) => {
|
||||
setViewMode('flow');
|
||||
setFocusCity(city);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Period selector — floating over map */}
|
||||
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
|
||||
<PeriodSelector
|
||||
|
||||
Reference in New Issue
Block a user