feat(classiques): support extended lines (1-3), bottom clock/info ticker (time=true), and dynamic SNCF Open Data logo resolution
This commit is contained in:
1 parent
427693c492
commit
5e9d770449
6 files changed
+981
-115
No files matched your search
@@ -7,6 +7,7 @@ import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
import { fetchAllRows } from '../../../lib/fetchAllRows';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
|
||||
|
||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
@@ -66,53 +67,61 @@ const formatStationName = (name) => {
|
||||
return str;
|
||||
};
|
||||
|
||||
const getTrainMeta = (horaire) => {
|
||||
const opRaw = String(horaire.type_train || horaire.op || horaire.mode || '').trim();
|
||||
const numTrain = String(horaire.numero_train || horaire.numero || horaire.number || horaire.trip_short_name || '').trim();
|
||||
const opUpper = opRaw.toUpperCase();
|
||||
|
||||
let logoSrc = '/type/logo-sncf.svg';
|
||||
let typeLabel = 'TER';
|
||||
|
||||
if (opUpper.includes('INOUI') || opUpper.includes('TGV')) {
|
||||
logoSrc = '/type/logo-inoui.svg';
|
||||
typeLabel = 'TGV inOUI';
|
||||
} else if (opUpper.includes('OUIGO')) {
|
||||
logoSrc = '/type/logo-ouigo.svg';
|
||||
typeLabel = 'OUIGO';
|
||||
} else if (opUpper.includes('MOBIGO')) {
|
||||
logoSrc = '/type/logo-sncf.svg';
|
||||
typeLabel = 'MOBIGO';
|
||||
} else if (opUpper.includes('ICE')) {
|
||||
logoSrc = '/type/logo-ice.svg';
|
||||
typeLabel = 'ICE';
|
||||
} else if (opUpper.includes('LYRIA')) {
|
||||
logoSrc = '/type/logo-lyria.svg';
|
||||
typeLabel = 'TGV Lyria';
|
||||
} else if (opUpper.includes('INTERCIT') || opUpper.includes('IC')) {
|
||||
logoSrc = '/type/logo-sncf.svg';
|
||||
typeLabel = 'INTERCITÉS';
|
||||
} else if (opUpper.includes('CAR') || opUpper.includes('AUTOCAR') || opUpper.includes('BUS')) {
|
||||
logoSrc = '/type/logo-sncf.svg';
|
||||
typeLabel = 'AUTOCAR';
|
||||
} else if (opRaw) {
|
||||
logoSrc = '/type/logo-sncf.svg';
|
||||
typeLabel = opRaw;
|
||||
}
|
||||
|
||||
// Si c'est un numéro typique MOBIGO en région BFC (séries 89xxxx, 84xxxx)
|
||||
if (typeLabel === 'TER' && (numTrain.startsWith('89') || numTrain.startsWith('84'))) {
|
||||
typeLabel = 'MOBIGO';
|
||||
}
|
||||
|
||||
return { logoSrc, typeLabel, numTrain };
|
||||
};
|
||||
|
||||
const formatPlatform = (p) => {
|
||||
if (!p) return '';
|
||||
return String(p).toUpperCase().replace(/VOIE|QUAI|\s+/gi, '').trim();
|
||||
};
|
||||
|
||||
const formatStopsForDeparts = (horaire, currentStationName, normalizeFn) => {
|
||||
const stops = (horaire && (horaire.stops || horaire.gares_desservies)) || [];
|
||||
if (!Array.isArray(stops) || stops.length === 0) {
|
||||
const dest = formatStationName((horaire && (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination)) || '');
|
||||
return dest ? `• ${dest}` : '';
|
||||
}
|
||||
|
||||
const curNorm = normalizeFn ? normalizeFn(currentStationName || '') : String(currentStationName || '').toLowerCase().trim();
|
||||
let startIdx = 0;
|
||||
if (curNorm) {
|
||||
const foundIdx = stops.findIndex(s => {
|
||||
const sName = (s.nom || s.gare_nom || s.name || s.stop_name || '').toString();
|
||||
const norm = normalizeFn ? normalizeFn(sName) : sName.toLowerCase().trim();
|
||||
return norm === curNorm;
|
||||
});
|
||||
if (foundIdx >= 0) {
|
||||
startIdx = foundIdx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const remainingStops = stops.slice(startIdx);
|
||||
if (remainingStops.length === 0) {
|
||||
const dest = formatStationName((horaire && (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination)) || '');
|
||||
return dest ? `• ${dest}` : '';
|
||||
}
|
||||
|
||||
const stopNames = remainingStops.map(s => formatStationName(s.nom || s.gare_nom || s.name || s.stop_name || '')).filter(Boolean);
|
||||
return stopNames.map(n => `• ${n}`).join(' ');
|
||||
};
|
||||
|
||||
function BottomClock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__bottom-clock" aria-hidden>
|
||||
<span>{hh}</span>
|
||||
<span>{mm}</span>
|
||||
<span>{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Clock(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -1206,12 +1215,27 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
if (h._isEmpty) {
|
||||
return <li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key={`empty-${idx}`} style={{ opacity: 0 }}></li>;
|
||||
const isExtended = Boolean(h && h._isExtended);
|
||||
const isOdd = ((h && typeof h._slotIndex === 'number' ? h._slotIndex : idx) % 2 === 0);
|
||||
|
||||
if (h && h._isEmpty) {
|
||||
if (isExtended) {
|
||||
return (
|
||||
<li
|
||||
className={`aff-classiques-departs__row aff-classiques-departs__row--extended ${isOdd ? 'aff-classiques-departs__row--odd' : 'aff-classiques-departs__row--even'}`}
|
||||
key={`empty-${idx}`}
|
||||
style={{ opacity: 0 }}
|
||||
>
|
||||
<div className="aff-classiques-departs__extended-top"></div>
|
||||
<div className="aff-classiques-departs__extended-bottom"></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li className={`aff-classiques-departs__row ${isOdd ? 'aff-classiques-departs__row--odd' : 'aff-classiques-departs__row--even'}`} key={`empty-${idx}`} style={{ opacity: 0 }}></li>;
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainMeta(horaire);
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.departure_time || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '';
|
||||
const formattedDestination = formatStationName(destination);
|
||||
@@ -1231,20 +1255,91 @@ export default function ClassiquesDeparts(){
|
||||
const stationName = gareDisplay || horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null;
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
|
||||
const stopsText = isExtended ? formatStopsForDeparts(horaire, gareDisplay || gareParam, normalizeName) : '';
|
||||
|
||||
if (isExtended) {
|
||||
return (
|
||||
<li
|
||||
className={`aff-classiques-departs__row aff-classiques-departs__row--extended ${isOdd ? 'aff-classiques-departs__row--odd' : 'aff-classiques-departs__row--even'}`}
|
||||
key={idx}
|
||||
>
|
||||
{/* Ligne haute (108px) : infos complètes */}
|
||||
<div className="aff-classiques-departs__extended-top">
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-departs__op-logo" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-meta">
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-departs__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-departs__train-number">{numTrain}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={formattedDestination} className="aff-classiques-departs__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ligne basse (108px) : gares desservies défilantes */}
|
||||
<div className="aff-classiques-departs__extended-bottom">
|
||||
<div className="aff-classiques-departs__sub-stops">
|
||||
<MarqueeText text={stopsText} className="aff-classiques-departs__sub-stops-text" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-departs__row" key={idx}>
|
||||
<li className={`aff-classiques-departs__row ${isOdd ? 'aff-classiques-departs__row--odd' : 'aff-classiques-departs__row--even'}`} key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-departs__op-logo" />
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro */}
|
||||
{/* COL 2 : Type + Numéro OU Statut */}
|
||||
<div className="aff-classiques-departs__col-meta">
|
||||
<div className="aff-classiques-departs__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-departs__train-number">{numTrain}</div>
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-departs__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-departs__train-number">{numTrain}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
@@ -1270,11 +1365,44 @@ export default function ClassiquesDeparts(){
|
||||
);
|
||||
};
|
||||
|
||||
const displayList = [...horaires.slice(0, 10)];
|
||||
while (displayList.length < 10) {
|
||||
displayList.push({ _isEmpty: true });
|
||||
// Gestion des paramètres extended et time
|
||||
const queryParams = new URLSearchParams(window.location.search || '');
|
||||
const extendedParam = parseInt(queryParams.get('extended') || queryParams.get('extendedLines') || '0', 10);
|
||||
const isTimeEnabled = queryParams.get('time') === 'true' || queryParams.get('time') === '1';
|
||||
|
||||
const maxSlots = isTimeEnabled ? 9 : 10;
|
||||
const extendedCount = Math.min(Math.max(0, extendedParam), 3);
|
||||
|
||||
const displayRows = [];
|
||||
let trainIdx = 0;
|
||||
let usedSlots = 0;
|
||||
|
||||
for (let i = 0; i < extendedCount && usedSlots + 2 <= maxSlots; i++) {
|
||||
const train = (horaires && horaires[trainIdx]) || { _isEmpty: true };
|
||||
displayRows.push({
|
||||
...train,
|
||||
_isExtended: true,
|
||||
_slotIndex: usedSlots
|
||||
});
|
||||
usedSlots += 2;
|
||||
trainIdx++;
|
||||
}
|
||||
|
||||
while (usedSlots < maxSlots) {
|
||||
const train = (horaires && horaires[trainIdx]) || { _isEmpty: true };
|
||||
displayRows.push({
|
||||
...train,
|
||||
_isExtended: false,
|
||||
_slotIndex: usedSlots
|
||||
});
|
||||
usedSlots += 1;
|
||||
trainIdx++;
|
||||
}
|
||||
|
||||
const infoMessage = (perturbations && perturbations.length > 0)
|
||||
? perturbations.map(p => p.titre ? `${p.titre} : ${p.message || p.description || ''}` : p.message || '').join(' • ')
|
||||
: "Trafic normal sur l'ensemble de la ligne • SNCF Voyageurs vous souhaite un agréable voyage.";
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__container">
|
||||
|
||||
@@ -1288,7 +1416,7 @@ export default function ClassiquesDeparts(){
|
||||
<div className="aff-classiques-departs__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
{displayRows.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
@@ -1299,9 +1427,17 @@ export default function ClassiquesDeparts(){
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
/* ── MODE NORMAL : 9 ou 10 lignes ── */
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
{displayRows.map((h, idx) => renderRow(h, idx))}
|
||||
{isTimeEnabled && (
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--info-bottom">
|
||||
<div className="aff-classiques-departs__info-bar">
|
||||
<MarqueeText text={infoMessage} className="aff-classiques-departs__info-bar-text" />
|
||||
</div>
|
||||
<BottomClock />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user