import { useEffect, useState } from 'react'; import { api } from '../../api/client'; import usePushSubscription from '../../hooks/usePushSubscription'; import './SpotPanel.css'; function scoreLabel(value, level, distance) { if (level === 'high') return 'Impact'; if (level === 'medium') return 'Risque'; if (value >= 15 && distance != null && distance < 100) return 'Veille'; return 'OK'; } const TYPE_ICON = { beach: '🏖', port: '⚓', surf: '🏄', fishing: '🎣' }; const TREND_CONFIG = { increasing: { icon: '📈', label: 'Situation en dégradation', cls: 'trend--bad' }, stable: { icon: '➡️', label: 'Situation stable', cls: 'trend--neutral' }, decreasing: { icon: '📉', label: 'Amélioration en cours', cls: 'trend--good' }, }; const ETA_BUFFER_KM = 5; function computeEta(score, horizons) { if (score?.distance != null && score.distance < ETA_BUFFER_KM) return 0; for (const h of [6, 12, 24, 48]) { const horizon = horizons?.[`h${h}`]; if (horizon?.distanceKm != null && horizon.distanceKm < ETA_BUFFER_KM) return h; } return null; } function etaLabel(etaHours, score, distance) { if (etaHours === 0) return { icon: '🚨', label: 'Impact en cours', cls: 'eta--now' }; if (etaHours === 6) return { icon: '⚠️', label: 'Impact prévu dans ~6h', cls: 'eta--soon' }; if (etaHours === 12) return { icon: '⚠️', label: 'Impact prévu dans ~12h', cls: 'eta--soon' }; if (etaHours === 24) return { icon: '🕐', label: 'Impact prévu dans ~24h', cls: 'eta--later' }; if (etaHours === 48) return { icon: '🕐', label: 'Impact prévu dans ~48h', cls: 'eta--later' }; if ((score ?? 0) >= 15 && distance != null && distance < 100) return { icon: '👁', label: 'Sargasses en surveillance', cls: 'eta--watch' }; return { icon: '✅', label: 'Aucun impact prévu (48h)', cls: 'eta--safe' }; } const HORIZON_LABELS = { now: 'Maintenant', h6: '+6h', h12: '+12h', h24: '+24h', h48: '+48h' }; export default function SpotPanel({ spot, activeStep, onClose }) { const [data, setData] = useState(undefined); const [error, setError] = useState(null); useEffect(() => { if (!spot) return; setData(undefined); setError(null); api.spots.score(spot.id) .then(d => setData(d)) .catch(() => setError('Impossible de charger le score.')); }, [spot?.id]); if (!spot) return null; // Score à afficher selon l'étape active const displayScore = activeStep === 'now' ? data?.score : (data?.horizons?.[activeStep] ? { value: data.horizons[activeStep].score, level: data.horizons[activeStep].level, distance: data.horizons[activeStep].distanceKm, confidence: data.horizons[activeStep].confidence, breakdown: data.horizons[activeStep].breakdown, } : null); const lvl = displayScore?.level; return (
{TYPE_ICON[spot.type] ?? '📍'}

{spot.name}

{spot.region}

{HORIZON_LABELS[activeStep] ?? activeStep} {activeStep !== 'now' && displayScore?.confidence != null && ( Confiance : {Math.round(displayScore.confidence * 100)}% )}
{data === undefined &&

Chargement…

} {error &&

{error}

} {data !== undefined && !error && ( displayScore === null ?

Aucune donnée pour cet horizon.

: ( <> {activeStep === 'now' && (() => { const eta = etaLabel(computeEta(data?.score, data?.horizons), data?.score?.value, data?.score?.distance); return (
{eta.icon} {eta.label}
); })()}

Indice de risque

{scoreLabel(displayScore.value, lvl, displayScore.distance)}

{displayScore.value} / 100

{activeStep === 'now' && displayScore.trend && (() => { const t = TREND_CONFIG[displayScore.trend] ?? TREND_CONFIG.stable; return (
{t.icon} {t.label}
); })()}
{displayScore.distance != null && ( <>
Distance sargasses
{displayScore.distance} km
)} {activeStep === 'now' && displayScore.computedAt && ( <>
Calculé le
{new Date(displayScore.computedAt).toLocaleString('fr-FR')}
)}
{activeStep === 'now' && } ) )}
); } function PatchList({ patches }) { if (!patches?.length) return null; // Échelle visuelle : 100 km = barre pleine const MAX_KM = 100; return (

{patches.length} banc{patches.length > 1 ? 's' : ''} de sargasses détecté{patches.length > 1 ? 's' : ''}

{patches.map((p, i) => { const pct = Math.min((p.distanceKm / MAX_KM) * 100, 100); const sizeLabel = p.areaKm2 >= 10 ? `${p.areaKm2} km²` : `${p.areaKm2} km²`; return (
{p.distanceKm} km
{sizeLabel}
); })}
); } const BREAKDOWN_ITEMS = [ { key: 'distance', label: 'Distance', max: 40, icon: '📏' }, { key: 'density', label: 'Densité', max: 30, icon: '🌿' }, { key: 'velocity', label: 'Vitesse', max: 20, icon: '➡' }, { key: 'trend', label: 'Tendance', max: 10, icon: '📈' }, ]; function ScoreBreakdown({ breakdown }) { if (!breakdown) return null; return (

Détail du score

{BREAKDOWN_ITEMS.map(({ key, label, max, icon }) => { const val = breakdown[key] ?? 0; const pct = max > 0 ? (val / max) * 100 : 0; return (
{icon} {label}
{val}/{max}
); })}
); } function FeedbackButton({ spot }) { const [sent, setSent] = useState(false); const [loading, setLoading] = useState(false); const submit = async (hasSeaweed) => { if (loading || sent) return; setLoading(true); if (spot.latitude != null && spot.longitude != null) { await api.feedback.create({ lat: spot.latitude, lng: spot.longitude, hasSeaweed, coastalPointId: spot.id, }).catch(console.error); } setLoading(false); setSent(true); }; if (sent) return

Merci pour votre retour !

; return (

Vous êtes sur place ?

); } function PushButton({ spotId }) { const { supported, subscribed, loading, error, toggle } = usePushSubscription(spotId); if (!supported) return null; return (
{error &&

{error}

}
); }