feat(design): refonte page département + AnomalyBadge, paliers soutenir
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m31s
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m31s
- Page département : nouveau design issu du mockup hi-fi (hero v-block coloré, "Que faire ?", autres alertes compactes, section repliable "tous au vert", tableau aujourd'hui/demain, actions rapides) - AnomalyBadge : refonte avec CSS tokens (v-block coloré, grand nombre, inline mode supprimé) - /soutenir : 3 paliers progressifs (10 €, 40 €, 150 €) avec barres de progression individuelles et indicateur d'objectif actif Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d5c0b0968d
commit
003b49c297
3 changed files with 436 additions and 260 deletions
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import VigilanceChip from '../../components/VigilanceChip.astro';
|
||||
import VigilanceLegend from '../../components/VigilanceLegend.astro';
|
||||
import { getVigilanceSnapshot, alertsForDepartement, currentEcheance } from '../../lib/vigilance';
|
||||
import { getDepartement, isDrom } from '../../lib/departements';
|
||||
import { PHENOMENA, COLOR_LABEL } from '../../lib/phenomena';
|
||||
import { PHENOMENA, COLOR_LABEL, COLORS } from '../../lib/phenomena';
|
||||
import type { PhenomenonId, ColorId } from '../../lib/phenomena';
|
||||
import { ADVICE, EMERGENCY_NUMBERS } from '../../lib/advice';
|
||||
import { getClimatoForDepartement } from '../../lib/climato';
|
||||
import { computeAnomaly, normaleForDate, normalesForRange } from '../../lib/normales';
|
||||
|
|
@ -25,8 +25,6 @@ Astro.response.headers.set('Cache-Control', 'public, max-age=60, must-revalidate
|
|||
|
||||
const drom = isDrom(dept.code);
|
||||
|
||||
// Fetch en parallèle des 3 sources externes (vigilance MF, climato data.gouv, hourly MF SYNOP).
|
||||
// Cold-fetch climato = ~10s (4 MB de previous CSV.GZ), parallel ramène le total à max(individuel).
|
||||
const [snapshotR, climatoR, hourlyR] = drom
|
||||
? [Promise.resolve(null), Promise.resolve(null), Promise.resolve(null)] as const
|
||||
: ([
|
||||
|
|
@ -61,7 +59,6 @@ const last30 = climato?.days?.slice(-30) ?? [];
|
|||
const last365 = climato?.days?.slice(-365) ?? [];
|
||||
|
||||
if (!drom) {
|
||||
// Tous les lookups normales sont locaux (JSON statique) — pas besoin de paralléliser plus.
|
||||
if (climato?.days?.length) {
|
||||
anomaly = await computeAnomaly(dept.code, climato.days);
|
||||
}
|
||||
|
|
@ -79,201 +76,341 @@ if (!drom) {
|
|||
|
||||
const stationLabel = hourly ? `${hourly.stationName} (${hourly.distKm} km)` : null;
|
||||
|
||||
// "Aujourd'hui" = écheance courante (J ou J1 selon l'heure)
|
||||
// "Demain" = J1 si on est encore sur J, sinon pas dispo (bulletin n'a pas J2)
|
||||
const ech = snapshot ? currentEcheance(snapshot) : 'J';
|
||||
const today = snapshot ? alertsForDepartement(snapshot, dept.code, ech) : [];
|
||||
const tomorrow = (snapshot && ech === 'J') ? alertsForDepartement(snapshot, dept.code, 'J1') : [];
|
||||
const highest = today[0];
|
||||
const adviceFor = highest && ADVICE[highest.phenomenonId];
|
||||
|
||||
const labelDate = (iso: string) => new Date(iso).toLocaleDateString('fr-FR', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', timeZone: 'Europe/Paris',
|
||||
});
|
||||
// Sort by severity desc, pick top alert for the hero block
|
||||
const sortedToday = [...today].sort((a, b) => b.colorId - a.colorId);
|
||||
const topAlert = sortedToday[0] ?? null;
|
||||
const otherAlerts = sortedToday.slice(1);
|
||||
|
||||
const adviceFor = topAlert ? ADVICE[topAlert.phenomenonId] : null;
|
||||
// First 3 action items as quick bullets inside the hero block
|
||||
const quickActions = adviceFor
|
||||
? adviceFor.blocks.flatMap((b) => b.items).slice(0, 3)
|
||||
: [];
|
||||
|
||||
// Per-phenomenon color maps for the comparison table
|
||||
const PHENOM_IDS: PhenomenonId[] = [1, 2, 3, 5, 6, 8, 9];
|
||||
const todayByPhenom = new Map<PhenomenonId, ColorId>();
|
||||
for (const a of today) {
|
||||
const prev = todayByPhenom.get(a.phenomenonId) ?? 0;
|
||||
if (a.colorId > prev) todayByPhenom.set(a.phenomenonId, a.colorId);
|
||||
}
|
||||
const tomorrowByPhenom = new Map<PhenomenonId, ColorId>();
|
||||
for (const a of tomorrow) {
|
||||
const prev = tomorrowByPhenom.get(a.phenomenonId) ?? 0;
|
||||
if (a.colorId > prev) tomorrowByPhenom.set(a.phenomenonId, a.colorId);
|
||||
}
|
||||
|
||||
const comparisonRows = PHENOM_IDS.map((id) => ({
|
||||
id,
|
||||
phen: PHENOMENA[id],
|
||||
todayColor: (todayByPhenom.get(id) ?? 1) as ColorId,
|
||||
tomorrowColor: ech === 'J' ? ((tomorrowByPhenom.get(id) ?? 1) as ColorId) : null,
|
||||
changed: ech === 'J' && (todayByPhenom.get(id) ?? 1) !== (tomorrowByPhenom.get(id) ?? 1),
|
||||
}));
|
||||
|
||||
// Phenomena with no active alert today (green)
|
||||
const activePhenomIds = new Set(today.map((a) => a.phenomenonId));
|
||||
const inactivePhenomIds = PHENOM_IDS.filter((id) => !activePhenomIds.has(id));
|
||||
|
||||
const productDate = snapshot?.productDatetime
|
||||
? new Date(snapshot.productDatetime).toLocaleString('fr-FR', {
|
||||
dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris',
|
||||
})
|
||||
: null;
|
||||
|
||||
const fmtTime = (iso: string) =>
|
||||
new Date(iso).toLocaleString('fr-FR', {
|
||||
dateStyle: 'short', timeStyle: 'short', timeZone: 'Europe/Paris',
|
||||
});
|
||||
|
||||
const labelDate = (iso: string) =>
|
||||
new Date(iso).toLocaleDateString('fr-FR', {
|
||||
weekday: 'long', day: 'numeric', month: 'long', timeZone: 'Europe/Paris',
|
||||
});
|
||||
|
||||
const todayLabel = today[0] ? labelDate(today[0].beginTime) : '';
|
||||
const tomorrowLabel = tomorrow[0] ? labelDate(tomorrow[0].beginTime) : '';
|
||||
|
||||
// Glyphs matching the pill levels
|
||||
const GLYPHS: Record<ColorId, string> = { 1: '●', 2: '▲', 3: '◆', 4: '■' };
|
||||
|
||||
// Pre-computed display values for the hero alert block
|
||||
const topColorName = topAlert ? COLORS[topAlert.colorId].name : null;
|
||||
const topPhen = topAlert ? PHENOMENA[topAlert.phenomenonId] : null;
|
||||
---
|
||||
|
||||
<Base
|
||||
title={`${dept.name} (${dept.code}) — Vigilance météo`}
|
||||
description={`Niveau de Vigilance Météo France pour ${dept.name} (${dept.region}) et conseils officiels.`}
|
||||
>
|
||||
<section class="bg-gradient-to-b from-canicule-50 to-white">
|
||||
<div class="container-tight py-8">
|
||||
<a href="/" class="text-sm text-canicule-700">← Retour à la carte</a>
|
||||
<h1 class="mt-2 text-3xl font-bold sm:text-4xl">{dept.name}</h1>
|
||||
<p class="text-slate-600">{dept.region} — département {dept.code}</p>
|
||||
{/* ── Hero header ── */}
|
||||
<section class="container-tight" style="padding-block: clamp(24px, 4vw, 48px) 20px;">
|
||||
<a href="/" class="ic-btn ic-btn-ghost ic-btn-sm" style="text-decoration: none; margin-bottom: 20px; display: inline-flex;">
|
||||
← Retour à la carte
|
||||
</a>
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; margin-bottom: 28px;">
|
||||
<div class="kicker">{dept.region} · département {dept.code}</div>
|
||||
<h1>{dept.name}</h1>
|
||||
{productDate && (
|
||||
<p class="kicker" style="text-transform: none; letter-spacing: 0; font-size: 0.82rem;">
|
||||
Bulletin Météo France du {productDate}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container-tight py-8">
|
||||
{/* ── Vigilance status ── */}
|
||||
<section class="container-tight" style="padding-block: 0 32px;">
|
||||
|
||||
{/* DROM notice */}
|
||||
{drom && (
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<p class="font-semibold text-amber-900">Vigilance Outre-mer non couverte par cette source</p>
|
||||
<p class="mt-1 text-sm text-amber-800">
|
||||
<div class="v-block v-jaune" style="margin-bottom: 24px;">
|
||||
<strong style="color: var(--v-jaune-ink);">▲ Vigilance Outre-mer non couverte par cette source</strong>
|
||||
<p style="color: var(--v-jaune-ink); margin-top: 8px; font-size: 0.95rem;">
|
||||
Les départements et régions d'Outre-mer disposent de leur propre dispositif Vigilance, géré
|
||||
par les centres météorologiques locaux de Météo France. Ces données ne sont pas (encore)
|
||||
rediffusées en open data au même format que la métropole.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
<a
|
||||
href="https://vigilance.meteofrance.fr/"
|
||||
rel="noopener"
|
||||
class="inline-flex items-center gap-1 rounded bg-amber-700 px-4 py-2 text-sm font-semibold text-white no-underline hover:bg-amber-800"
|
||||
>
|
||||
Voir la Vigilance officielle Outre-mer →
|
||||
</a>
|
||||
<a
|
||||
href="https://vigilance.meteofrance.fr/"
|
||||
rel="noopener"
|
||||
class="ic-btn ic-btn-sm"
|
||||
style="text-decoration: none; margin-top: 14px; background: var(--v-jaune-ink); color: var(--v-jaune-bg); border-color: var(--v-jaune-ink);"
|
||||
>
|
||||
Voir la Vigilance officielle Outre-mer →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{!drom && error && (
|
||||
<div class="v-block v-rouge" style="margin-bottom: 24px;">
|
||||
<strong style="color: var(--v-rouge-ink);">■ Données momentanément indisponibles</strong>
|
||||
<p style="color: var(--v-rouge-ink); margin-top: 8px; font-size: 0.92rem;">
|
||||
Réessayez dans quelques minutes. En urgence, consultez
|
||||
<a href="https://vigilance.meteofrance.fr/" rel="noopener" style="color: inherit; text-decoration: underline;">vigilance.meteofrance.fr</a>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!drom && error && <p class="text-red-700">Données indisponibles : {error}</p>}
|
||||
|
||||
{
|
||||
!drom && !error && today.length === 0 && (
|
||||
<div class="rounded border border-green-200 bg-green-50 p-4">
|
||||
<p class="font-semibold text-green-800">
|
||||
Aucune vigilance particulière {todayLabel ? `pour ${todayLabel}` : "aujourd'hui"}.
|
||||
</p>
|
||||
<p class="text-sm text-green-700">Le département est en niveau vert pour tous les phénomènes.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!drom && !error && today.length > 0 && (
|
||||
<>
|
||||
<h2 class="mb-3 text-xl font-semibold capitalize">
|
||||
Alertes en cours {todayLabel ? `— ${todayLabel}` : ''}
|
||||
</h2>
|
||||
<ul class="space-y-3">
|
||||
{today.map((a) => {
|
||||
const phen = PHENOMENA[a.phenomenonId];
|
||||
const begin = new Date(a.beginTime).toLocaleString('fr-FR', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
timeZone: 'Europe/Paris',
|
||||
});
|
||||
const end = new Date(a.endTime).toLocaleString('fr-FR', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short',
|
||||
timeZone: 'Europe/Paris',
|
||||
});
|
||||
return (
|
||||
<li class="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="text-lg font-semibold">
|
||||
<span aria-hidden="true">{phen.emoji}</span> {phen.label}
|
||||
</div>
|
||||
<VigilanceChip colorId={a.colorId} showLevel />
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-slate-600">
|
||||
Valide du {begin} au {end} (heure de Paris).
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!drom && !error && tomorrow.length > 0 && (
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-xl font-semibold capitalize">
|
||||
Prévision {tomorrowLabel ? `— ${tomorrowLabel}` : 'pour demain'}
|
||||
</h2>
|
||||
<ul class="space-y-2">
|
||||
{tomorrow.map((a) => (
|
||||
<li class="flex flex-wrap items-center justify-between gap-3 rounded border border-slate-200 px-3 py-2">
|
||||
<span>{PHENOMENA[a.phenomenonId].label}</span>
|
||||
<VigilanceChip colorId={a.colorId} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
{
|
||||
climato && climato.days.length > 0 && (
|
||||
<section class="border-t border-slate-200 bg-slate-50">
|
||||
<div class="container-tight py-8">
|
||||
<h2 class="mb-4 text-xl font-semibold">Températures récentes</h2>
|
||||
{anomaly && (
|
||||
<div class="mb-4">
|
||||
<AnomalyBadge anomaly={anomaly} />
|
||||
</div>
|
||||
)}
|
||||
<TemperatureChartInteractive
|
||||
hourly={hourly}
|
||||
days7={last7}
|
||||
days30={last30}
|
||||
days365={last365}
|
||||
normales7={normales7}
|
||||
normales30={normales30}
|
||||
normales365={normales365}
|
||||
normaleHourly={normaleHourly}
|
||||
stationLabel={stationLabel}
|
||||
/>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
Sources : Météo France — observations SYNOP horaires (onglet 24 h) et données
|
||||
climatologiques de base quotidiennes (7 j / 30 j), agrégées par moyenne sur les
|
||||
stations du département. Normales calculées sur 1991-2020 (référence WMO).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
adviceFor && (
|
||||
<section class="border-t border-slate-200 bg-white">
|
||||
<div class="container-tight py-8">
|
||||
<h2 class="text-xl font-semibold">Conseils — {PHENOMENA[highest!.phenomenonId].label}</h2>
|
||||
<p class="mt-2 text-slate-700">{adviceFor.intro}</p>
|
||||
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
{adviceFor.blocks.map((block) => (
|
||||
<div class="rounded-lg border border-slate-200 p-4">
|
||||
<h3 class="font-semibold text-slate-900">{block.title}</h3>
|
||||
<ul class="mt-2 list-inside list-disc space-y-1 text-sm text-slate-700">
|
||||
{block.items.map((item) => (
|
||||
<li>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
{!drom && !error && (
|
||||
<>
|
||||
{/* All-green state */}
|
||||
{sortedToday.length === 0 && (
|
||||
<div class="v-block v-vert" style="margin-bottom: 24px;">
|
||||
<h2 style="color: var(--v-vert-ink); font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);">
|
||||
● Aucune vigilance active {todayLabel ? `pour ${todayLabel}` : "aujourd'hui"}
|
||||
</h2>
|
||||
<p style="color: var(--v-vert-ink); margin-top: 10px; line-height: 1.5;">
|
||||
Tous les phénomènes sont au vert pour {dept.name}. Restez attentif aux mises à jour Météo France.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{adviceFor.emergency.length > 0 && (
|
||||
<div class="mt-6 rounded-lg border border-red-200 bg-red-50 p-4">
|
||||
<h3 class="font-semibold text-red-900">En cas d'urgence</h3>
|
||||
<ul class="mt-2 space-y-1 text-sm text-red-800">
|
||||
{adviceFor.emergency.map((e) => (
|
||||
<li>{e}</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* Hero — top alert */}
|
||||
{topAlert && topColorName && topPhen && (
|
||||
<div class={`v-block v-${topColorName}`} style="margin-bottom: 24px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
||||
<span style="font-size: 2rem; line-height: 1;" aria-hidden="true">{topPhen.emoji}</span>
|
||||
<h2 style={`color: var(--v-${topColorName}-ink); font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);`}>
|
||||
{GLYPHS[topAlert.colorId]} {topPhen.label} — {COLOR_LABEL[topAlert.colorId]}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mt-6">
|
||||
<h3 class="text-sm font-semibold text-slate-700">Numéros utiles</h3>
|
||||
<div class="mt-2 grid gap-2 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
||||
{adviceFor && (
|
||||
<div style="margin-top: 4px; padding: 16px 20px; background: var(--paper-2); border-radius: var(--r-md); color: var(--ink);">
|
||||
<strong style="font-size: 0.95rem;">Que faire maintenant ?</strong>
|
||||
<ul style="margin-top: 10px; padding-left: 18px; color: var(--ink-2); line-height: 1.6;">
|
||||
{quickActions.map((a) => <li>{a}</li>)}
|
||||
</ul>
|
||||
<a
|
||||
href={`/conseils/${topPhen.slug}`}
|
||||
style="display: inline-flex; align-items: center; gap: 6px; margin-top: 12px; font-size: 0.88rem; font-weight: 600; color: var(--brand-deep); text-decoration: none;"
|
||||
>
|
||||
Voir le kit complet {topPhen.label} →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={`color: var(--v-${topColorName}-ink); margin-top: 14px; font-size: 0.85rem; opacity: 0.85;`}>
|
||||
Valide du {fmtTime(topAlert.beginTime)} au {fmtTime(topAlert.endTime)} — heure de Paris.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other active alerts */}
|
||||
{otherAlerts.map((a) => {
|
||||
const colorName = COLORS[a.colorId].name;
|
||||
const phen = PHENOMENA[a.phenomenonId];
|
||||
return (
|
||||
<div class={`v-block v-${colorName}`} style="margin-bottom: 12px; padding: 16px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 1.3rem;" aria-hidden="true">{phen.emoji}</span>
|
||||
<strong style={`color: var(--v-${colorName}-ink);`}>{phen.label}</strong>
|
||||
</div>
|
||||
<VigilanceChip colorId={a.colorId} showLevel />
|
||||
</div>
|
||||
<p style={`color: var(--v-${colorName}-ink); margin-top: 8px; font-size: 0.82rem; opacity: 0.8;`}>
|
||||
Du {fmtTime(a.beginTime)} au {fmtTime(a.endTime)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Collapsible: inactive phenomena (all green) */}
|
||||
{inactivePhenomIds.length > 0 && (
|
||||
<details class="ic-card" style="margin-bottom: 28px; padding: 16px;">
|
||||
<summary style="cursor: pointer; font-weight: 600; color: var(--ink-2); display: flex; align-items: center; justify-content: space-between; list-style: none;">
|
||||
<span>
|
||||
{inactivePhenomIds.length} phénomène{inactivePhenomIds.length > 1 ? 's' : ''} — tous au vert
|
||||
</span>
|
||||
<span aria-hidden="true" style="color: var(--ink-mute); font-size: 0.8rem;">▼</span>
|
||||
</summary>
|
||||
<div class="grid gap-2" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); margin-top: 14px;">
|
||||
{inactivePhenomIds.map((id) => (
|
||||
<div class="v-block v-vert" style="padding: 10px 14px; font-size: 0.9rem; display: flex; align-items: center; gap: 8px;">
|
||||
<span aria-hidden="true">{PHENOMENA[id].emoji}</span>
|
||||
<span style="color: var(--v-vert-ink);">{PHENOMENA[id].label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Today vs Tomorrow comparison */}
|
||||
{(today.length > 0 || tomorrow.length > 0 || ech === 'J') && (
|
||||
<div class="ic-card" style="margin-bottom: 28px; overflow-x: auto;">
|
||||
<h3 style="margin-bottom: 16px;">
|
||||
Aujourd'hui{todayLabel ? ` — ${todayLabel}` : ''} vs Demain{tomorrowLabel ? ` — ${tomorrowLabel}` : ''}
|
||||
</h3>
|
||||
<div class="comparison-grid" style="display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 6px 12px; align-items: center; min-width: 360px;">
|
||||
<span class="kicker">Phénomène</span>
|
||||
<span class="kicker" style="text-align: center;">Aujourd'hui</span>
|
||||
{ech === 'J'
|
||||
? <span class="kicker" style="text-align: center;">Demain</span>
|
||||
: <span class="kicker" style="text-align: center; opacity: 0.4;">Demain</span>
|
||||
}
|
||||
{comparisonRows.map(({ id, phen, todayColor, tomorrowColor, changed }) => (
|
||||
<>
|
||||
<span style="font-size: 0.9rem; font-weight: 500; display: flex; align-items: center; gap: 7px;">
|
||||
<span aria-hidden="true">{phen.emoji}</span>{phen.label}
|
||||
</span>
|
||||
<span style="text-align: center;">
|
||||
<VigilanceChip colorId={todayColor} />
|
||||
</span>
|
||||
<span style="text-align: center;">
|
||||
{tomorrowColor !== null
|
||||
? (
|
||||
<span style="display: inline-flex; flex-direction: column; align-items: center; gap: 2px;">
|
||||
<VigilanceChip colorId={tomorrowColor} />
|
||||
{changed && <span style="font-size: 0.72rem; color: var(--brand-deep); font-weight: 600;">change</span>}
|
||||
</span>
|
||||
)
|
||||
: <span style="color: var(--ink-mute); font-size: 0.85rem;">—</span>
|
||||
}
|
||||
</span>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Emergency numbers if there's an active alert */}
|
||||
{topAlert && EMERGENCY_NUMBERS.length > 0 && (
|
||||
<div class="ic-card-soft" style="margin-bottom: 28px; padding: 18px;">
|
||||
<div class="kicker" style="margin-bottom: 12px;">Numéros d'urgence</div>
|
||||
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));">
|
||||
{EMERGENCY_NUMBERS.map((n) => (
|
||||
<div class="flex items-baseline gap-2">
|
||||
<a href={`tel:${n.num}`} class="font-mono font-bold text-canicule-700">
|
||||
<div style="display: flex; align-items: baseline; gap: 8px;">
|
||||
<a href={`tel:${n.num}`} style="font-family: var(--font-mono); font-weight: 700; font-size: 1.15rem; color: var(--v-rouge); text-decoration: none; border-bottom: none;">
|
||||
{n.num}
|
||||
</a>
|
||||
<span class="text-slate-600">{n.label}</span>
|
||||
<span style="color: var(--ink-soft); font-size: 0.88rem;">{n.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Température & climato ── */}
|
||||
{climato && climato.days.length > 0 && (
|
||||
<section class="container-tight" style="padding-block: 0 32px;">
|
||||
<div class="ic-card" style="padding: clamp(16px, 3vw, 28px);">
|
||||
<h2 style="font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem); margin-bottom: 20px;">Températures récentes</h2>
|
||||
<TemperatureChartInteractive
|
||||
hourly={hourly}
|
||||
days7={last7}
|
||||
days30={last30}
|
||||
days365={last365}
|
||||
normales7={normales7}
|
||||
normales30={normales30}
|
||||
normales365={normales365}
|
||||
normaleHourly={normaleHourly}
|
||||
stationLabel={stationLabel}
|
||||
/>
|
||||
{anomaly && (
|
||||
<div style="margin-top: 20px;">
|
||||
<AnomalyBadge anomaly={anomaly} />
|
||||
</div>
|
||||
)}
|
||||
<p style="margin-top: 12px; font-size: 0.82rem; color: var(--ink-soft);">
|
||||
Sources : Météo France — observations SYNOP horaires (onglet 24 h) et données
|
||||
climatologiques de base quotidiennes (7 j / 30 j / 365 j), agrégées par station la plus
|
||||
proche. Normales 1991-2020 (référence WMO).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Actions rapides ── */}
|
||||
{!drom && !error && (
|
||||
<section class="container-tight" style="padding-block: 0 32px;">
|
||||
<div class="grid gap-3 actions-grid">
|
||||
{topAlert && (
|
||||
<a
|
||||
href={`/conseils/${PHENOMENA[topAlert.phenomenonId].slug}`}
|
||||
class="ic-card ic-card-interactive"
|
||||
style="text-decoration: none; color: inherit; padding: 20px; display: flex; flex-direction: column; gap: 8px;"
|
||||
>
|
||||
<span style="font-size: 1.6rem;" aria-hidden="true">{PHENOMENA[topAlert.phenomenonId].emoji}</span>
|
||||
<strong style="font-size: 1rem;">Kit {PHENOMENA[topAlert.phenomenonId].label}</strong>
|
||||
<span style="color: var(--ink-soft); font-size: 0.88rem; line-height: 1.4;">Conseils officiels, imprimables et partageables</span>
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href="/conseils"
|
||||
class="ic-card ic-card-interactive"
|
||||
style="text-decoration: none; color: inherit; padding: 20px; display: flex; flex-direction: column; gap: 8px;"
|
||||
>
|
||||
<span style="font-size: 1.6rem;" aria-hidden="true">📋</span>
|
||||
<strong style="font-size: 1rem;">Tous les conseils</strong>
|
||||
<span style="color: var(--ink-soft); font-size: 0.88rem; line-height: 1.4;">7 phénomènes Vigilance — kits à imprimer et à partager</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://vigilance.meteofrance.fr/"
|
||||
rel="noopener"
|
||||
class="ic-card ic-card-interactive"
|
||||
style="text-decoration: none; color: inherit; padding: 20px; display: flex; flex-direction: column; gap: 8px;"
|
||||
>
|
||||
<span style="font-size: 1.6rem;" aria-hidden="true">🔗</span>
|
||||
<strong style="font-size: 1rem;">Source officielle</strong>
|
||||
<span style="color: var(--ink-soft); font-size: 0.88rem; line-height: 1.4;">vigilance.meteofrance.fr — bulletin complet Météo France</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.actions-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
||||
details[open] summary span[aria-hidden] { transform: rotate(180deg); }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
</style>
|
||||
</Base>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue