diff --git a/src/App.tsx b/src/App.tsx index 90e5ee2..a797b9e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import type { TransactionArc } from './data/arcData'; import { computeStats } from './data/mockData'; import { computeFlowStats } from './data/arcData'; import { useAnimation } from './hooks/useAnimation'; +import { useMediaQuery } from './hooks/useMediaQuery'; export default function App() { const [periodDays, setPeriodDays] = useState(7); @@ -25,6 +26,8 @@ export default function App() { const [allTimestamps, setAllTimestamps] = useState([]); const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap'); const [focusCity, setFocusCity] = useState(null); + const [panelOpen, setPanelOpen] = useState(false); + const isMobile = useMediaQuery('(max-width: 639px)'); const animation = useAnimation(transactions, arcs, periodDays, allTimestamps); @@ -86,20 +89,22 @@ export default function App() { [arcs, animation.visibleArcs, animation.active], ); + const statsPanelProps = { + stats: visibleStats, + loading, + periodDays, + source, + currentUD, + animationLabel: animation.active ? (animation.currentFrame?.label ?? undefined) : undefined, + viewMode, + flowStats, + focusCity, + }; + return (
- {/* Side panel */} - + {/* Side panel — desktop uniquement */} + {!isMobile && } {/* Map area */}
@@ -113,8 +118,19 @@ export default function App() { /> )} + {/* Bouton menu — mobile uniquement */} + {isMobile && ( + + )} + {/* Period selector — floating over map */} -
+
- {/* Transaction count + source badge (masqués en mode animation) */} - {!loading && !animation.active && ( + {/* Badge ville focus — mobile uniquement */} + {isMobile && focusCity && ( +
+ {focusCity} + +
+ )} + + {/* Transaction count + source badge (masqués sur mobile et en mode animation) */} + {!loading && !animation.active && !isMobile && (
{transactions.length} transactions affichées @@ -168,6 +192,28 @@ export default function App() {
)}
+ + {/* Bottom drawer — mobile uniquement */} + {isMobile && ( + <> + {/* Overlay */} + {panelOpen && ( +
setPanelOpen(false)} + /> + )} + {/* Drawer */} +
+
+
+
+ setPanelOpen(false)} /> +
+ + )}
); } diff --git a/src/components/AnimationPlayer.tsx b/src/components/AnimationPlayer.tsx index 6e1817f..cf993f0 100644 --- a/src/components/AnimationPlayer.tsx +++ b/src/components/AnimationPlayer.tsx @@ -26,8 +26,8 @@ export function AnimationPlayer({ const frame = frames[currentIndex]; return ( -
-
+
+
{/* Frame label + position */}
@@ -50,7 +50,7 @@ export function AnimationPlayer({ /> {/* Controls row */} -
+
{/* Playback buttons */}
diff --git a/src/components/PeriodSelector.tsx b/src/components/PeriodSelector.tsx index ee5e847..a5e2e1d 100644 --- a/src/components/PeriodSelector.tsx +++ b/src/components/PeriodSelector.tsx @@ -41,13 +41,13 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi const isCustomActive = !PRESET_DAYS.has(value); return ( -
+
{PERIODS.map(({ label, days }) => ( + )}
{/* Description */} diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..093bfca --- /dev/null +++ b/src/hooks/useMediaQuery.ts @@ -0,0 +1,12 @@ +import { useState, useEffect } from 'react'; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + useEffect(() => { + const mq = window.matchMedia(query); + const handler = (e: MediaQueryListEvent) => setMatches(e.matches); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [query]); + return matches; +}