diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 5d1276e..292d50d 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,23 +1,32 @@
import { useState } from 'react';
import SargassesMap from './components/Map/SargassesMap';
import SpotPanel from './components/SpotPanel/SpotPanel';
-import TimelineSlider from './components/Timeline/TimelineSlider';
+import Topbar from './components/Topbar/Topbar';
+import Legend from './components/Legend/Legend';
import './App.css';
export default function App() {
const [selectedSpot, setSelectedSpot] = useState(null);
const [activeStep, setActiveStep] = useState('now');
+ const [flyToTarget, setFlyToTarget] = useState(null);
return (
-
+
+
+
+
+
+ Zone observée
+
+
+
+ Prévision dérive
+
+
+
+
+ OK
+
+
+
+ Risque
+
+
+
+ Impact
+
+
+ );
+}
diff --git a/frontend/src/components/Map/SargassesMap.jsx b/frontend/src/components/Map/SargassesMap.jsx
index 8ba7797..af8dc30 100644
--- a/frontend/src/components/Map/SargassesMap.jsx
+++ b/frontend/src/components/Map/SargassesMap.jsx
@@ -3,17 +3,19 @@ import Map, { Layer, Marker, NavigationControl, Source } from 'react-map-gl/mapl
import 'maplibre-gl/dist/maplibre-gl.css';
import { api } from '../../api/client';
-const INITIAL_VIEW = { longitude: -61.2, latitude: 15.5, zoom: 7 };
+const INITIAL_VIEW = { longitude: -61.55, latitude: 16.25, zoom: 9 };
-// Styles des couches selon le type (observation vs prédiction)
-const layerStyles = (isPrediction) => ({
+const LEVEL_COLOR = { low: '#22c55e', medium: '#eab308', high: '#ef4444' };
+
+const makeLayerStyles = (isPrediction, opacity) => ({
fill: {
id: isPrediction ? 'forecasts-fill' : 'observations-fill',
type: 'fill',
source: isPrediction ? 'forecasts' : 'observations',
paint: {
'fill-color': isPrediction ? '#818cf8' : '#f59e0b',
- 'fill-opacity': isPrediction ? 0.25 : 0.35,
+ 'fill-opacity': (isPrediction ? 0.25 : 0.35) * opacity,
+ 'fill-opacity-transition': { duration: 350, delay: 0 },
},
},
line: {
@@ -23,30 +25,63 @@ const layerStyles = (isPrediction) => ({
paint: {
'line-color': isPrediction ? '#6366f1' : '#d97706',
'line-width': 2,
+ 'line-opacity': opacity,
+ 'line-opacity-transition': { duration: 350, delay: 0 },
'line-dasharray': isPrediction ? [4, 3] : [1],
},
},
});
-const OBS_STYLES = layerStyles(false);
-const FORE_STYLES = layerStyles(true);
-
const horizonToInt = (h) => parseInt(h.replace('h', ''), 10);
-export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep }) {
+export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep, flyToTarget }) {
const mapRef = useRef(null);
- const [spots, setSpots] = useState([]);
- const [obsGeoJSON, setObsGeoJSON] = useState(null);
- const [foreGeoJSON, setForeGeoJSON] = useState(null);
- const [viewState, setViewState] = useState(INITIAL_VIEW);
+ const [spots, setSpots] = useState([]);
+ const [spotScores, setSpotScores] = useState({});
+ const [obsGeoJSON, setObsGeoJSON] = useState(null);
+ const [foreGeoJSON, setForeGeoJSON] = useState(null);
+ const [viewState, setViewState] = useState(INITIAL_VIEW);
+ const [layerOpacity, setLayerOpacity] = useState(1);
- // Spots côtiers : chargement unique
+ // Spots : chargement unique
useEffect(() => {
api.spots.list()
.then(data => setSpots(Array.isArray(data) ? data : (data['hydra:member'] ?? data.member ?? [])))
.catch(console.error);
}, []);
+ // Scores des spots pour colorier les marqueurs (chargement progressif)
+ useEffect(() => {
+ if (spots.length === 0) return;
+ spots.forEach(s => {
+ api.spots.score(s.id)
+ .then(d => {
+ const level = d?.score?.level;
+ if (level) setSpotScores(prev => ({ ...prev, [s.id]: level }));
+ })
+ .catch(() => {});
+ });
+ }, [spots]);
+
+ // Fade transition quand on change d'étape
+ useEffect(() => {
+ setLayerOpacity(0);
+ const t = setTimeout(() => setLayerOpacity(1), 60);
+ return () => clearTimeout(t);
+ }, [activeStep]);
+
+ // flyTo quand une île est sélectionnée
+ useEffect(() => {
+ if (!flyToTarget) return;
+ const map = mapRef.current?.getMap();
+ if (!map) return;
+ map.flyTo({
+ center: flyToTarget.center,
+ zoom: flyToTarget.zoom,
+ speed: 1.4,
+ });
+ }, [flyToTarget]);
+
const loadLayers = useCallback(() => {
const map = mapRef.current?.getMap();
if (!map) return;
@@ -56,13 +91,11 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep
.map(v => v.toFixed(4)).join(',');
if (activeStep === 'now') {
- // Observations réelles
api.observations.list(bbox)
.then(data => setObsGeoJSON(toFeatureCollection(data.items)))
.catch(console.error);
setForeGeoJSON(null);
} else {
- // Forecasts pour cet horizon
const horizon = horizonToInt(activeStep);
api.forecasts.list(bbox, horizon)
.then(data => {
@@ -89,6 +122,9 @@ export default function SargassesMap({ onSpotSelect, selectedSpotId, activeStep
return null;
};
+ const OBS_STYLES = makeLayerStyles(false, layerOpacity);
+ const FORE_STYLES = makeLayerStyles(true, layerOpacity);
+
return (