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:
1 parent
19868a451f
commit
6dfd798dd2
16 files changed
+6177
-2030
No files matched your search
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user