RELEASE V3.0.0 : Voici les modifications apportées :

-> Refonte complète des afficheurs AFL départ et arrivée (+ Quais)
-> (ALPHA) : Implémentation des afficheurs EVA (Départ et arrivée uniquement pour l'instant).
This commit is contained in:
mathis committed 2026-08-28 04:07:19 +02:00
1 parent 19868a451f
commit 6dfd798dd2
16 files changed
+6177 -2030

No files matched your search

+273 -174
View File
@@ -3,6 +3,7 @@ import supabase from '../../../lib/supabase';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLDepartsQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
@@ -667,7 +668,6 @@ const AFLDepartsQuais = () => {
const map = buildPerturbMap(filteredPerturb, parsedHoraires);
const annotated = applyPerturbationsToHoraires(finalList, filteredPerturb, null, gareDisplay, serviceDateStr, map);
// Now filter by platform specific to this quai
const candidatesOnPlatform = annotated.filter(h => {
const stopsList = getStops(h);
let stopForStation = null;
@@ -675,7 +675,8 @@ const AFLDepartsQuais = () => {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
if (!quaiParam) return true;
return platform.toLowerCase() === quaiParam.toLowerCase();
});
if (candidatesOnPlatform.length > 0) {
@@ -696,202 +697,300 @@ const AFLDepartsQuais = () => {
return () => clearInterval(interval);
}, [fetchData]);
if (loading) return <div className="afl-quais-departs__container" style={{background: '#042054'}}></div>;
if (!nextDeparture) return (
<div className="afl-quais-departs__container">
<div className="afl-quais-departs__header">
<div className="afl-quais-departs__header-left">
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
</svg>
<div className="afl-quais-departs__header-title">Prochain départ</div>
</div>
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
if (loading) return <div className="afl-q-container" style={{ background: '#0c0f2e' }}></div>;
if (!nextDeparture) {
return (
<div className="afl-q-container">
<header className="afl-q-header">
<div className="afl-q-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-header-icon">
<path d="M4 8l5 5-5 5M11 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="21" cy="11" r="2.6" fill="white" />
<path d="M19 15h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M26 6v20M26 6h2M26 26h2" stroke="white" strokeWidth="2.2" strokeLinecap="round" />
</svg>
<div className="afl-q-header-title">Prochain départ</div>
</div>
<div className="afl-q-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '4.0vh', color: '#ffffff', fontWeight: 600 }}>
Aucun départ prévu
</div>
<div className="afl-quais-departs__no-departure">Aucun départ prévu</div>
</div>
);
<footer className="afl-q-footer">
{quaiParam && (
<div className="afl-q-voie-box">
<div className="afl-q-voie-label">Voie</div>
<div className="afl-q-voie-value">{quaiParam}</div>
</div>
)}
</footer>
</div>
);
}
const stops = getStops(nextDeparture);
let filteredStops = [];
const trainData = nextDeparture;
const isSuppressed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('supprimé');
const isDelayed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('retard');
if (stops.length > 0 && gareDisplay) {
const stationIndex = stops.findIndex(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
if (stationIndex !== -1) {
filteredStops = stops.slice(stationIndex + 1);
} else {
const firstStopName = normalizeName((stops[0].gare_nom || stops[0].name || stops[0].nom || '').toString());
if (firstStopName && firstStopName === normalizeName(gareDisplay)) {
filteredStops = stops.slice(1);
}
}
let delayVal = '+ 5 min';
if (typeof trainData._status?.delayMinutes === 'number' && trainData._status.delayMinutes > 0) {
delayVal = `+ ${trainData._status.delayMinutes} min`;
} else if (trainData._status?.label && trainData._status.label.includes('+')) {
delayVal = trainData._status.label;
}
const destName = nextDeparture.gare_arrivee_nom || nextDeparture.gare_arrivee || 'Terminus';
if (filteredStops.length > 0) {
const lastStopName = normalizeName((filteredStops[filteredStops.length - 1].gare_nom || filteredStops[filteredStops.length - 1].name || filteredStops[filteredStops.length - 1].nom || '').toString());
const normDest = normalizeName(destName);
if (lastStopName !== normDest && !lastStopName.includes(normDest) && !normDest.includes(lastStopName)) {
filteredStops.push({ gare_nom: destName, isLast: true });
} else {
filteredStops[filteredStops.length - 1].isLast = true;
}
} else {
filteredStops.push({ gare_nom: destName, isLast: true });
}
const pillClass = isSuppressed ? 'afl-q-pill--suppressed' : isDelayed ? 'afl-q-pill--delayed' : '';
const pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayVal : "À l'heure";
// Deduplicate stops by name
const seenStops = new Set();
filteredStops = filteredStops.filter(s => {
const name = normalizeName((s.gare_nom || s.name || s.nom || s.stop_name || '').toString());
if (!name) return false;
if (seenStops.has(name)) return false;
seenStops.add(name);
return true;
});
const isSuppressed = nextDeparture._status && nextDeparture._status.label === 'supprimé';
const isDelayed = nextDeparture._status && nextDeparture._status.label && String(nextDeparture._status.label).toLowerCase().includes('retard');
let displayStatusText = "À l'heure";
if (isSuppressed) displayStatusText = "Supprimé";
else if (isDelayed) {
if (typeof nextDeparture._status.delayMinutes === 'number') {
displayStatusText = `+${nextDeparture._status.delayMinutes} min`;
} else {
displayStatusText = nextDeparture._status.label;
const renderPillIcon = () => {
if (isSuppressed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<path d="M6.5 6.5l7 7M13.5 6.5l-7 7" stroke="#ff5c73" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
}
}
if (isDelayed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<line x1="10" y1="4.5" x2="10" y2="11.5" stroke="white" strokeWidth="2.4" strokeLinecap="round" />
<circle cx="10" cy="14.8" r="1.4" fill="white" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="white" />
<path d="M6 10.2l2.6 2.6 5.4-5.4" stroke="#45527a" strokeWidth="2.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const displayTime = nextDeparture._displayTime || formatTimeNoSeconds(nextDeparture.heure_depart || nextDeparture.time || nextDeparture.heure || '');
const trainType = nextDeparture.type_train || nextDeparture.op || 'TER';
const trainNum = nextDeparture.numero_train || nextDeparture.numero || nextDeparture.number || '';
const stopsList = getStops(nextDeparture);
const displayTime = trainData._displayTime || formatTimeNoSeconds(trainData.heure_depart || trainData.time || trainData.heure || '');
const trainType = trainData.type_train || trainData.op || 'TER';
const trainNum = trainData.numero_train || trainData.numero || trainData.number || '';
const destName = trainData.gare_arrivee_nom || trainData.gare_arrivee || 'Terminus';
const stopsList = getStops(trainData);
let stopForStation = null;
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || nextDeparture.quai_reel || nextDeparture.voie || nextDeparture.quai || quaiParam || '').toString().trim();
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim();
const shouldScroll = filteredStops.length > 10;
// eslint-disable-next-line no-unused-vars
const scrollStyle = shouldScroll ? {
animation: `scrollStops ${filteredStops.length * 2.2}s linear infinite`
} : {};
// Extraction des destinations (prise en compte des trains multi-branches / tranches)
let destinations = [];
if (Array.isArray(trainData.tranches) && trainData.tranches.length > 0) {
destinations = trainData.tranches.map(t => ({
name: t.gare_arrivee_nom || t.destination || t.nom,
op: t.type_train || t.op || trainType,
number: t.numero_train || t.number || trainNum
}));
} else if (destName.includes('/')) {
destinations = destName.split('/').map(d => ({
name: d.trim(),
op: trainType,
number: trainNum
}));
} else if (destName.includes(' - ') && (destName.toLowerCase().includes('marseille') || destName.toLowerCase().includes('montpellier'))) {
destinations = [
{ name: 'Marseille Saint-Charles', op: trainType, number: trainNum },
{ name: 'Montpellier Saint-Roch', op: trainType, number: trainNum }
];
} else {
destinations = [{
name: destName,
op: trainType,
number: trainNum
}];
}
// Formatage des lignes de destinations (découpage élégant sur 2 lignes si nécessaire)
const formatDestLines = (name) => {
if (!name) return ['Terminus'];
const clean = name.trim();
if (clean.includes('Marseille Saint-Charles')) return ['Marseille Saint-', 'Charles'];
if (clean.includes('Montpellier Saint-Roch')) return ['Montpellier', 'Saint-Roch'];
if (clean.includes('Paris Gare de Lyon')) return ['Paris', 'Gare de Lyon'];
if (clean.includes('Paris Montparnasse')) return ['Paris', 'Montparnasse'];
if (clean.includes('Bourg-en-Bresse')) return ['Bourg-en-', 'Bresse'];
if (clean.length > 12) {
const parts = clean.split(' ');
if (parts.length >= 2) {
const mid = Math.ceil(parts.length / 2);
return [parts.slice(0, mid).join(' '), parts.slice(mid).join(' ')];
}
}
return [clean];
};
// Calcul dynamique des arrêts desservis (aval)
const currentStationName = gareDisplay || (stopForStation && (stopForStation.gare_nom || stopForStation.nom || stopForStation.name)) || (stopsList[0] && (stopsList[0].gare_nom || stopsList[0].nom || stopsList[0].name)) || 'Départ';
let downstreamStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) {
const normCurrent = normalizeName(currentStationName);
const startIdx = stopsList.findIndex(s => {
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
});
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList.slice(1);
const seen = new Set();
rawDownstream.forEach(s => {
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
const norm = normalizeName(name);
if (name && norm !== normCurrent && !seen.has(norm)) {
seen.add(norm);
downstreamStops.push(name);
}
});
}
// Si aucun arrêt intermédiaire n'est trouvé, afficher la/les destination(s)
if (downstreamStops.length === 0) {
if (destName) {
if (destName.includes('/')) {
downstreamStops = destName.split('/').map(d => d.trim());
} else {
downstreamStops = [destName];
}
}
}
return (
<div className="afl-quais-departs__container">
{/* Header */}
<div className="afl-quais-departs__header">
<div className="afl-quais-departs__header-left">
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
{/* Chevrons / motion lines */}
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
{/* Train icon */}
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
</svg>
<div className="afl-quais-departs__header-title">Prochain départ</div>
<div className="afl-q-container">
{/* HEADER (10.5vh) */}
<header className="afl-q-header">
<div className="afl-q-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-header-icon">
<path d="M4 8l5 5-5 5M11 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="21" cy="11" r="2.6" fill="white" />
<path d="M19 15h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M26 6v20M26 6h2M26 26h2" stroke="white" strokeWidth="2.2" strokeLinecap="round" />
</svg>
<div className="afl-q-header-title">Prochain départ</div>
</div>
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
</div>
<div className="afl-q-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div className="afl-quais-departs__main">
{/* Left Pane (Floating Card) */}
<div className="afl-quais-departs__left-pane">
<div className="afl-quais-departs__time-status">
<div className="afl-quais-departs__time">{displayTime}</div>
<div className={`afl-quais-departs__status-pill ${isSuppressed ? 'afl-quais-departs__status-pill--suppressed' : (isDelayed ? 'afl-quais-departs__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#005eb8" />
</svg>
)}
<span>{displayStatusText}</span>
{/* CORPS PRINCIPAL */}
<main className="afl-q-main">
{/* PANNEAU GAUCHE : CARTE D'INFORMATION DU TRAIN */}
<section className="afl-q-left-card">
{/* Ligne Heure de départ & Statut */}
<div className="afl-q-card-top">
<div className="afl-q-card-time">{displayTime}</div>
<div className={`afl-q-pill ${pillClass}`}>
{renderPillIcon()}
<span>{pillText}</span>
</div>
</div>
<div className="afl-quais-departs__destination">{destName}</div>
<div className="afl-quais-departs__train-info">
<span className="afl-quais-departs__train-type">{trainType}</span>
<span className="afl-quais-departs__train-number">{trainNum}</span>
</div>
</div>
{/* Right Pane (Timeline) */}
<div className="afl-quais-departs__right-pane">
<div className="afl-quais-departs__stops-window">
<style>
{`
@keyframes scrollStops {
0%, 15% { transform: translateY(0); }
85%, 100% { transform: translateY(calc(-100% + 65vh)); }
}
`}
</style>
<ul className="afl-quais-departs__stops-list" style={shouldScroll ? { animation: `scrollStops ${filteredStops.length * 1.5}s ease-in-out infinite alternate` } : {}}>
{/* Main List */}
{filteredStops.map((s, i) => {
const sname = s.gare_nom || s.nom || s.name || s.stop_name || '';
const isFirst = !shouldScroll && i === 0;
const isLastItem = s.isLast || i === filteredStops.length - 1;
const isLast = !shouldScroll && isLastItem;
{/* Blocs Destinations dynamiques selon l'horaire */}
{destinations.map((d, i) => {
const lines = formatDestLines(d.name);
return (
<div key={i} className="afl-q-dest-block">
{lines.map((line, lIdx) => (
<div key={lIdx} className="afl-q-dest-title">{line}</div>
))}
<div className="afl-q-dest-sub">
{d.op} {d.number}
</div>
</div>
);
})}
{/* Pied de carte : Logo du type de train (sans icônes de services) */}
<div className="afl-q-card-footer">
<Logo
op={trainType}
classNameBox="afl-q-train-logo-box"
classNameImg="afl-q-train-logo-img"
classNameText="afl-q-train-logo-text"
/>
</div>
</section>
{/* PANNEAU DROIT : TIMELINE ARBORESCENTE DYNAMIQUE */}
<section className="afl-q-timeline-area">
<svg viewBox="0 0 850 560" className="afl-q-timeline-svg">
{/* 3 mini-points cyan au sommet */}
<circle cx="45" cy="22" r="4" fill="#00c5ff" />
<circle cx="45" cy="38" r="4" fill="#00c5ff" />
<circle cx="45" cy="54" r="4" fill="#00c5ff" />
{/* Station 1: Gare actuelle */}
<circle cx="45" cy="92" r="17" fill="#0c102a" stroke="#00c5ff" strokeWidth="4" />
<path d="M 37 89 L 45 97 L 53 89" stroke="#00c5ff" strokeWidth="3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<text x="82" y="102" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{currentStationName}
</text>
{/* Tracé dynamique des arrêts aval */}
{downstreamStops.length <= 1 ? (
<g>
<line x1="45" y1="109" x2="45" y2="480" stroke="#00c5ff" strokeWidth="5.5" strokeLinecap="round" />
<circle cx="45" cy="480" r="16" fill="#0c102a" stroke="#00c5ff" strokeWidth="5" />
<text x="82" y="490" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{downstreamStops[0] || destName}
</text>
</g>
) : (
(() => {
const N = downstreamStops.length;
const yStart = 160;
const yEnd = 500;
const step = Math.min(65, (yEnd - yStart) / Math.max(1, N - 1));
const lineEndY = yStart + (N - 1) * step;
return (
<li key={`s1-${i}`} className={`afl-quais-departs__stop-item ${isFirst ? 'is-first' : ''} ${isLast ? 'is-last' : ''}`}>
<div className="afl-quais-departs__stop-indicator">
<div className="afl-quais-departs__stop-line"></div>
<div className={`afl-quais-departs__stop-dot ${isLastItem ? 'is-terminus' : ''}`}></div>
</div>
<div className={`afl-quais-departs__stop-name ${isLastItem ? 'is-terminus' : ''}`}>{sname}</div>
</li>
<g>
<line x1="45" y1="109" x2="45" y2={lineEndY} stroke="#00c5ff" strokeWidth="5.5" strokeLinecap="round" />
{downstreamStops.map((stopName, sIdx) => {
const isLast = sIdx === N - 1;
const posY = yStart + sIdx * step;
if (!isLast) {
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="6.5" fill="#00c5ff" />
<text x="82" y={posY + 8} fill="#ffffff" fontSize="28" fontWeight="500" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
}
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="16" fill="#0c102a" stroke="#00c5ff" strokeWidth="5" />
<text x="82" y={posY + 10} fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
})}
</g>
);
})}
</ul>
</div>
</div>
</div>
{/* Symmetrical train outline under the stops list */}
<div className="afl-quais-departs__bottom-container">
<div className="afl-quais-departs__train-illustration">
<svg viewBox="0 0 260 40" width="22vw" height="3.5vh">
<path fill="none" stroke="#00c3e3" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
d="M30,35 L230,35 C242,35 248,31 254,22 L258,12 C260,8 256,4 247,4 L13,4 C4,4 0,8 2,12 L6,22 C12,31 18,35 30,35 Z" />
<line x1="55" y1="4" x2="55" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="82" y1="4" x2="82" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="109" y1="4" x2="109" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="136" y1="4" x2="136" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="163" y1="4" x2="163" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="190" y1="4" x2="190" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="217" y1="4" x2="217" y2="35" stroke="#00c3e3" strokeWidth="2" />
})()
)}
</svg>
</section>
</main>
{/* FOOTER : BOX VOIE EN BAS À DROITE (SANS REPÈRES DE QUAI NI COMPOSITION) */}
<footer className="afl-q-footer">
<div className="afl-q-voie-box">
<div className="afl-q-voie-label">Voie</div>
<div className="afl-q-voie-value">{platformStr}</div>
</div>
</div>
{platformStr && (
<div className="afl-quais-departs__platform-box">
<div className="afl-quais-departs__platform-label">Voie</div>
<div className="afl-quais-departs__platform-value">{platformStr}</div>
</div>
)}
</footer>
</div>
);
};