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
@@ -25,6 +25,10 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
corresp: false
|
||||
});
|
||||
|
||||
// Options spécifiques pour les afficheurs Classiques
|
||||
const [extendedLines, setExtendedLines] = useState(0); // 0, 1, 2, 3
|
||||
const [showTime, setShowTime] = useState(false);
|
||||
|
||||
const toggleAflPage = (key) => {
|
||||
setAflPages(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
@@ -82,6 +86,13 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
if (activeKeys.length > 0) {
|
||||
url += `&pages=${activeKeys.join(',')}`;
|
||||
}
|
||||
} else if (typeParam === 'classiques') {
|
||||
if (extendedLines > 0) {
|
||||
url += `&extended=${extendedLines}`;
|
||||
}
|
||||
if (showTime) {
|
||||
url += `&time=true`;
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url);
|
||||
@@ -96,7 +107,18 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
}
|
||||
if (typeof navigate === 'function') {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`);
|
||||
let url = `${prefix}/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`;
|
||||
|
||||
if (typeParam === 'classiques') {
|
||||
if (extendedLines > 0) {
|
||||
url += `&extended=${extendedLines}`;
|
||||
}
|
||||
if (showTime) {
|
||||
url += `&time=true`;
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,6 +254,68 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeParam === 'classiques' && (
|
||||
<div className="launcher-checklist-box">
|
||||
<div className="launcher-checklist-header">
|
||||
<span className="launcher-checklist-title">
|
||||
Options d'affichage Classiques :
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Choix du nombre de lignes étendues */}
|
||||
<div style={{ marginBottom: '14px' }}>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '8px' }}>
|
||||
Lignes étendues (Dessertes défilantes) :
|
||||
</label>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}>
|
||||
{[0, 1, 2, 3].map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
type="button"
|
||||
onClick={() => setExtendedLines(num)}
|
||||
style={{
|
||||
padding: '8px 4px',
|
||||
borderRadius: '8px',
|
||||
border: extendedLines === num ? '2px solid #00439e' : '1px solid #c2dbfe',
|
||||
background: extendedLines === num ? '#00439e' : '#ffffff',
|
||||
color: extendedLines === num ? '#ffffff' : '#002257',
|
||||
fontWeight: 700,
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
{num === 0 ? '0 (Standard)' : `${num} étendue${num > 1 ? 's' : ''}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#555', marginTop: '6px' }}>
|
||||
{extendedLines > 0
|
||||
? `Les ${extendedLines} premier${extendedLines > 1 ? 's' : ''} train${extendedLines > 1 ? 's' : ''} s'afficheront sur 2 lignes réduites avec le défilement de toutes les gares.`
|
||||
: 'Affichage classique compact sans défilement de gares.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option Afficher l'heure */}
|
||||
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '10px' }}>
|
||||
<label className="launcher-checkbox-label" style={{ cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTime}
|
||||
onChange={(e) => setShowTime(e.target.checked)}
|
||||
className="launcher-checkbox"
|
||||
/>
|
||||
<span>
|
||||
<strong>Afficher l'heure & informations</strong>
|
||||
</span>
|
||||
</label>
|
||||
<div style={{ fontSize: '12px', color: '#555', marginLeft: '28px', marginTop: '2px' }}>
|
||||
Réserve la 10e ligne pour l'horloge numérique [ HH MM SS ] et un bandeau d'information défilant (horaires sur 9 lignes max).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Chaque ligne (108px de haut) */
|
||||
/* Chaque ligne simple (108px de haut) */
|
||||
.aff-classiques-arrivees__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -75,10 +75,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__row--odd,
|
||||
.aff-classiques-arrivees__row:nth-child(odd) {
|
||||
background: var(--green-odd);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__row--even,
|
||||
.aff-classiques-arrivees__row:nth-child(even) {
|
||||
background: var(--green-even);
|
||||
}
|
||||
@@ -87,6 +89,74 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ── LIGNE ÉTENDUE (216px = 2 lignes réduites) ── */
|
||||
.aff-classiques-arrivees__row--extended {
|
||||
height: 216px;
|
||||
min-height: 216px;
|
||||
max-height: 216px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__extended-top {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 20px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__row--odd .aff-classiques-arrivees__extended-top {
|
||||
background: var(--green-odd);
|
||||
}
|
||||
.aff-classiques-arrivees__row--even .aff-classiques-arrivees__extended-top {
|
||||
background: var(--green-even);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__extended-bottom {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 200px;
|
||||
padding-right: 280px;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__row--odd .aff-classiques-arrivees__extended-bottom {
|
||||
background: var(--green-even);
|
||||
}
|
||||
.aff-classiques-arrivees__row--even .aff-classiques-arrivees__extended-bottom {
|
||||
background: var(--green-odd);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__sub-stops {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__sub-stops-text {
|
||||
color: var(--white);
|
||||
font-size: 52px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
font-family: "Achemine", sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
.aff-classiques-arrivees__col-logo {
|
||||
width: 105px;
|
||||
@@ -106,7 +176,7 @@
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
/* Colonne 2 : Type de train + Numéro */
|
||||
/* Colonne 2 : Type de train + Numéro OU Statut */
|
||||
.aff-classiques-arrivees__col-meta {
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
@@ -139,6 +209,32 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-ontime {
|
||||
color: var(--white);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed {
|
||||
color: var(--yellow);
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-suppressed {
|
||||
color: #ff4d4d;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-arrivees__col-time {
|
||||
width: 290px;
|
||||
@@ -223,3 +319,55 @@
|
||||
font-family: "Achemine", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── 10e LIGNE : BANDEAU D'INFORMATION & HORLOGE (time=true) ── */
|
||||
.aff-classiques-arrivees__row--info-bottom {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 24px;
|
||||
padding-right: 20px;
|
||||
background: #0d3613 !important;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__info-bar {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__info-bar-text {
|
||||
color: var(--white);
|
||||
font-size: 44px;
|
||||
font-weight: 600;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__bottom-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 10px;
|
||||
padding: 6px 18px;
|
||||
background: #09260d;
|
||||
font-family: "Achemine", sans-serif;
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
letter-spacing: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -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 formatStopsForArrivees = (horaire, currentStationName, normalizeFn) => {
|
||||
const stops = (horaire && (horaire.stops || horaire.gares_desservies)) || [];
|
||||
if (!Array.isArray(stops) || stops.length === 0) {
|
||||
const orig = formatStationName((horaire && (horaire.gare_depart_nom || horaire.gare_depart || horaire.origine)) || '');
|
||||
return orig ? `• ${orig}` : '';
|
||||
}
|
||||
|
||||
const curNorm = normalizeFn ? normalizeFn(currentStationName || '') : String(currentStationName || '').toLowerCase().trim();
|
||||
let endIdx = stops.length;
|
||||
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) {
|
||||
endIdx = foundIdx;
|
||||
}
|
||||
}
|
||||
|
||||
const precedingStops = stops.slice(0, endIdx);
|
||||
if (precedingStops.length === 0) {
|
||||
const orig = formatStationName((horaire && (horaire.gare_depart_nom || horaire.gare_depart || horaire.origine)) || '');
|
||||
return orig ? `• ${orig}` : '';
|
||||
}
|
||||
|
||||
const stopNames = precedingStops.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-arrivees__bottom-clock" aria-hidden>
|
||||
<span>{hh}</span>
|
||||
<span>{mm}</span>
|
||||
<span>{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Clock(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -1209,12 +1218,27 @@ export default function ClassiquesArrivees(){
|
||||
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
if (h._isEmpty) {
|
||||
return <li className="aff-classiques-arrivees__row aff-classiques-arrivees__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-arrivees__row aff-classiques-arrivees__row--extended ${isOdd ? 'aff-classiques-arrivees__row--odd' : 'aff-classiques-arrivees__row--even'}`}
|
||||
key={`empty-${idx}`}
|
||||
style={{ opacity: 0 }}
|
||||
>
|
||||
<div className="aff-classiques-arrivees__extended-top"></div>
|
||||
<div className="aff-classiques-arrivees__extended-bottom"></div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li className={`aff-classiques-arrivees__row ${isOdd ? 'aff-classiques-arrivees__row--odd' : 'aff-classiques-arrivees__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_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || horaire.origine || '';
|
||||
const formattedOrigin = formatStationName(origin);
|
||||
@@ -1234,20 +1258,91 @@ export default function ClassiquesArrivees(){
|
||||
const stationName = gareDisplay || horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination_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 ? formatStopsForArrivees(horaire, gareDisplay || gareParam, normalizeName) : '';
|
||||
|
||||
if (isExtended) {
|
||||
return (
|
||||
<li
|
||||
className={`aff-classiques-arrivees__row aff-classiques-arrivees__row--extended ${isOdd ? 'aff-classiques-arrivees__row--odd' : 'aff-classiques-arrivees__row--even'}`}
|
||||
key={idx}
|
||||
>
|
||||
{/* Ligne haute (108px) : infos complètes */}
|
||||
<div className="aff-classiques-arrivees__extended-top">
|
||||
<div className="aff-classiques-arrivees__col-logo">
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-arrivees__op-logo" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-meta">
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-arrivees__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-arrivees__train-number">{numTrain}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-time">
|
||||
<div className="aff-classiques-arrivees__time">{time}</div>
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-destination">
|
||||
<MarqueeText text={formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-arrivees__platform-box">
|
||||
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ligne basse (108px) : gares de provenance défilantes */}
|
||||
<div className="aff-classiques-arrivees__extended-bottom">
|
||||
<div className="aff-classiques-arrivees__sub-stops">
|
||||
<MarqueeText text={stopsText} className="aff-classiques-arrivees__sub-stops-text" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-arrivees__row" key={idx}>
|
||||
<li className={`aff-classiques-arrivees__row ${isOdd ? 'aff-classiques-arrivees__row--odd' : 'aff-classiques-arrivees__row--even'}`} key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-arrivees__col-logo">
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-arrivees__op-logo" />
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro */}
|
||||
{/* COL 2 : Type + Numéro OU Statut */}
|
||||
<div className="aff-classiques-arrivees__col-meta">
|
||||
<div className="aff-classiques-arrivees__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-arrivees__train-number">{numTrain}</div>
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-arrivees__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-arrivees__train-number">{numTrain}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
@@ -1273,11 +1368,44 @@ export default function ClassiquesArrivees(){
|
||||
);
|
||||
};
|
||||
|
||||
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-arrivees__container">
|
||||
|
||||
@@ -1291,7 +1419,7 @@ export default function ClassiquesArrivees(){
|
||||
<div className="aff-classiques-arrivees__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-arrivees__list">
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
{displayRows.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
@@ -1302,9 +1430,17 @@ export default function ClassiquesArrivees(){
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
/* ── MODE NORMAL : 9 ou 10 lignes ── */
|
||||
<ul className="aff-classiques-arrivees__list">
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
{displayRows.map((h, idx) => renderRow(h, idx))}
|
||||
{isTimeEnabled && (
|
||||
<li className="aff-classiques-arrivees__row aff-classiques-arrivees__row--info-bottom">
|
||||
<div className="aff-classiques-arrivees__info-bar">
|
||||
<MarqueeText text={infoMessage} className="aff-classiques-arrivees__info-bar-text" />
|
||||
</div>
|
||||
<BottomClock />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Chaque ligne (108px de haut) */
|
||||
/* Chaque ligne simple (108px de haut) */
|
||||
.aff-classiques-departs__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -75,10 +75,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row--odd,
|
||||
.aff-classiques-departs__row:nth-child(odd) {
|
||||
background: var(--blue-odd);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row--even,
|
||||
.aff-classiques-departs__row:nth-child(even) {
|
||||
background: var(--blue-even);
|
||||
}
|
||||
@@ -87,6 +89,74 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ── LIGNE ÉTENDUE (216px = 2 lignes réduites) ── */
|
||||
.aff-classiques-departs__row--extended {
|
||||
height: 216px;
|
||||
min-height: 216px;
|
||||
max-height: 216px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__extended-top {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 20px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row--odd .aff-classiques-departs__extended-top {
|
||||
background: var(--blue-odd);
|
||||
}
|
||||
.aff-classiques-departs__row--even .aff-classiques-departs__extended-top {
|
||||
background: var(--blue-even);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__extended-bottom {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 200px;
|
||||
padding-right: 280px;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row--odd .aff-classiques-departs__extended-bottom {
|
||||
background: var(--blue-even);
|
||||
}
|
||||
.aff-classiques-departs__row--even .aff-classiques-departs__extended-bottom {
|
||||
background: var(--blue-odd);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__sub-stops {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__sub-stops-text {
|
||||
color: var(--white);
|
||||
font-size: 52px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
font-family: "Achemine", sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
.aff-classiques-departs__col-logo {
|
||||
width: 105px;
|
||||
@@ -106,7 +176,7 @@
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
/* Colonne 2 : Type de train + Numéro */
|
||||
/* Colonne 2 : Type de train + Numéro OU Statut */
|
||||
.aff-classiques-departs__col-meta {
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
@@ -139,6 +209,32 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-ontime {
|
||||
color: var(--white);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed {
|
||||
color: var(--yellow);
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-suppressed {
|
||||
color: #ff4d4d;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-departs__col-time {
|
||||
width: 290px;
|
||||
@@ -223,3 +319,55 @@
|
||||
font-family: "Achemine", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── 10e LIGNE : BANDEAU D'INFORMATION & HORLOGE (time=true) ── */
|
||||
.aff-classiques-departs__row--info-bottom {
|
||||
height: 108px;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 24px;
|
||||
padding-right: 20px;
|
||||
background: #001e40 !important;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
border-top: 2px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__info-bar {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__info-bar-text {
|
||||
color: var(--white);
|
||||
font-size: 44px;
|
||||
font-weight: 600;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__bottom-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 10px;
|
||||
padding: 6px 18px;
|
||||
background: #00142e;
|
||||
font-family: "Achemine", sans-serif;
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
letter-spacing: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Indexation optimisée des logos pour recherche rapide et insensible à la casse / accents
|
||||
const normalizeStr = (s) => {
|
||||
if (!s) return '';
|
||||
return String(s)
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]/g, '');
|
||||
};
|
||||
|
||||
// Table des logos issue de public/type/data.json
|
||||
const LOGOS_DATA = [
|
||||
{ file: "logo-aleop.svg", slug: "aleop", name: "Aleop" },
|
||||
{ file: "logo-auvergerhonealpes.svg", slug: "auvergerhonealpes", name: "Auvergerhonealpes" },
|
||||
{ file: "logo-breizhgo.svg", slug: "breizhgo", name: "Breizhgo" },
|
||||
{ file: "logo-cfl.svg", slug: "cfl", name: "CFL" },
|
||||
{ file: "logo-db.svg", slug: "db", name: "DB" },
|
||||
{ file: "logo-eurostar.svg", slug: "eurostar", name: "Eurostar" },
|
||||
{ file: "logo-fluo.svg", slug: "fluo", name: "Fluo" },
|
||||
{ file: "logo-frecciarossa.svg", slug: "frecciarossa", name: "Frecciarossa" },
|
||||
{ file: "logo-hautsdefrance.svg", slug: "hautsdefrance", name: "Hautsdefrance" },
|
||||
{ file: "logo-ice.svg", slug: "ice", name: "ICE" },
|
||||
{ file: "logo-intercites.svg", slug: "intercites", name: "Intercités" },
|
||||
{ file: "logo-ler.svg", slug: "ler", name: "Ler" },
|
||||
{ file: "logo-lio.svg", slug: "lio", name: "Lio" },
|
||||
{ file: "logo-lunea.svg", slug: "lunea", name: "Lunea" },
|
||||
{ file: "logo-metrolor.svg", slug: "metrolor", name: "Metrolor" },
|
||||
{ file: "logo-mobigo.svg", slug: "mobigo", name: "Mobigo" },
|
||||
{ file: "logo-nomad.svg", slug: "nomad", name: "Nomad" },
|
||||
{ file: "logo-occitanie.svg", slug: "occitanie", name: "Occitanie" },
|
||||
{ file: "logo-ouigo.svg", slug: "ouigo", name: "TGV Ouigo" },
|
||||
{ file: "logo-ouigo-classique.svg", slug: "ouigo-classique", name: "OUIGO Trains Classiques" },
|
||||
{ file: "logo-remi.svg", slug: "remi", name: "Remi" },
|
||||
{ file: "logo-renfeave.svg", slug: "renfeave", name: "Renfe AVE" },
|
||||
{ file: "logo-rer-a.svg", slug: "rer-a", name: "RER A" },
|
||||
{ file: "logo-rer-b.svg", slug: "rer-b", name: "RER B" },
|
||||
{ file: "logo-rer-c.svg", slug: "rer-c", name: "RER C" },
|
||||
{ file: "logo-rer-d.svg", slug: "rer-d", name: "RER D" },
|
||||
{ file: "logo-rer-e.svg", slug: "rer-e", name: "RER E" },
|
||||
{ file: "logo-sbb.svg", slug: "sbb", name: "SBB" },
|
||||
{ file: "logo-sncb.svg", slug: "sncb", name: "SNCB" },
|
||||
{ file: "logo-sncf.svg", slug: "sncf", name: "Sncf" },
|
||||
{ file: "logo-sncf1985.svg", slug: "sncf1985", name: "Sncf1985" },
|
||||
{ file: "logo-sncf1992.svg", slug: "sncf1992", name: "Sncf1992" },
|
||||
{ file: "logo-teoz.svg", slug: "teoz", name: "Teoz" },
|
||||
{ file: "logo-ter.svg", slug: "ter", name: "TER" },
|
||||
{ file: "logo-ter-rhone-alpes.svg", slug: "ter-rhone-alpes", name: "TER Rhone-Alpes" },
|
||||
{ file: "logo-teralsace.svg", slug: "teralsace", name: "Ter Alsace" },
|
||||
{ file: "logo-teraquitaine.svg", slug: "teraquitaine", name: "Ter Aquitaine" },
|
||||
{ file: "logo-terbassenormandie.svg", slug: "terbassenormandie", name: "Ter Basse Normandie" },
|
||||
{ file: "logo-terbourgogne.svg", slug: "terbourgogne", name: "Ter Bourgogne" },
|
||||
{ file: "logo-terbretagne.svg", slug: "terbretagne", name: "Ter Bretagne" },
|
||||
{ file: "logo-tercentre.svg", slug: "tercentre", name: "Ter Centre" },
|
||||
{ file: "logo-terlanguedocroussillon.svg", slug: "terlanguedocroussillon", name: "Ter Languedoc-Roussillon" },
|
||||
{ file: "logo-termidipyrenees.svg", slug: "termidipyrenees", name: "Ter Midi-Pyrenees" },
|
||||
{ file: "logo-ternordpasdecalais.svg", slug: "ternordpasdecalais", name: "Ter Nord-pas-de-Calais" },
|
||||
{ file: "logo-terpoitoucharentes.svg", slug: "terpoitoucharentes", name: "Ter Poitou Charentes" },
|
||||
{ file: "logo-tgv.svg", slug: "tgv", name: "TGV" },
|
||||
{ file: "logo-inoui.svg", slug: "inoui", name: "TGV InOui" },
|
||||
{ file: "logo-lyria.svg", slug: "lyria", name: "TGV Lyria" },
|
||||
{ file: "logo-thalys.svg", slug: "thalys", name: "Thalys" },
|
||||
{ file: "logo-thello.svg", slug: "thello", name: "Thello" },
|
||||
{ file: "track-car.svg", slug: "track-car", name: "Car TER" },
|
||||
{ file: "logo-tramtrain.svg", slug: "tramtrain", name: "Tramtrain" },
|
||||
{ file: "logo-trenitalia.svg", slug: "trenitalia", name: "Trenitalia" },
|
||||
{ file: "logo-zou.svg", slug: "zou", name: "Zou" }
|
||||
];
|
||||
|
||||
const logoLookup = new Map();
|
||||
LOGOS_DATA.forEach((item) => {
|
||||
if (!item || !item.file) return;
|
||||
const file = item.file;
|
||||
const name = item.name || '';
|
||||
const slug = item.slug || '';
|
||||
|
||||
if (slug) logoLookup.set(normalizeStr(slug), { file, name, slug });
|
||||
if (name) logoLookup.set(normalizeStr(name), { file, name, slug });
|
||||
const cleanFile = file.replace(/^logo-/, '').replace(/\.svg$/, '').replace(/\.png$/, '');
|
||||
logoLookup.set(normalizeStr(cleanFile), { file, name, slug });
|
||||
});
|
||||
|
||||
/**
|
||||
* Détermine le logo SVG / PNG et le libellé de train à afficher à partir d'un objet horaire
|
||||
* Supporte les données SNCF Open Data (GTFS / SIRI) et Supabase
|
||||
*/
|
||||
export function getTrainLogoAndMeta(horaire) {
|
||||
if (!horaire) {
|
||||
return { logoSrc: '/type/logo-sncf.svg', typeLabel: 'TER', numTrain: '' };
|
||||
}
|
||||
|
||||
const opRaw = String(
|
||||
horaire.type_train ||
|
||||
horaire.op ||
|
||||
horaire.commercial_mode ||
|
||||
horaire.mode ||
|
||||
horaire.carrier ||
|
||||
horaire.transporteur ||
|
||||
''
|
||||
).trim();
|
||||
|
||||
const numTrain = String(
|
||||
horaire.numero_train ||
|
||||
horaire.numero ||
|
||||
horaire.number ||
|
||||
horaire.trip_short_name ||
|
||||
''
|
||||
).trim();
|
||||
|
||||
const opNorm = normalizeStr(opRaw);
|
||||
|
||||
// 1. Recherche directe dans la base des logos
|
||||
let matchedLogo = logoLookup.get(opNorm);
|
||||
|
||||
// 2. Recherche par mots-clés si pas de correspondance exacte
|
||||
if (!matchedLogo) {
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgv')) {
|
||||
matchedLogo = logoLookup.get('inoui') || logoLookup.get('tgv');
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
matchedLogo = logoLookup.get('ouigoclassique');
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
matchedLogo = logoLookup.get('ouigo');
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
matchedLogo = logoLookup.get('mobigo');
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
matchedLogo = logoLookup.get('fluo');
|
||||
} else if (opNorm.includes('remi')) {
|
||||
matchedLogo = logoLookup.get('remi');
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
matchedLogo = logoLookup.get('nomad');
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
matchedLogo = logoLookup.get('aleop');
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
matchedLogo = logoLookup.get('breizhgo');
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
matchedLogo = logoLookup.get('hautsdefrance');
|
||||
} else if (opNorm.includes('lio')) {
|
||||
matchedLogo = logoLookup.get('lio');
|
||||
} else if (opNorm.includes('zou')) {
|
||||
matchedLogo = logoLookup.get('zou');
|
||||
} else if (opNorm.includes('intercites') || opNorm.includes('ic')) {
|
||||
matchedLogo = logoLookup.get('intercites');
|
||||
} else if (opNorm.includes('ice')) {
|
||||
matchedLogo = logoLookup.get('ice');
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
matchedLogo = logoLookup.get('eurostar');
|
||||
} else if (opNorm.includes('thalys')) {
|
||||
matchedLogo = logoLookup.get('thalys');
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
matchedLogo = logoLookup.get('lyria');
|
||||
} else if (opNorm.includes('frecciarossa') || opNorm.includes('trenitalia')) {
|
||||
matchedLogo = logoLookup.get('frecciarossa') || logoLookup.get('trenitalia');
|
||||
} else if (opNorm.includes('renfe') || opNorm.includes('ave')) {
|
||||
matchedLogo = logoLookup.get('renfeave');
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
matchedLogo = logoLookup.get('trackcar') || { file: 'track-car.svg', name: 'Car TER' };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Détermination du libellé affiché (typeLabel)
|
||||
let typeLabel = opRaw || 'TER';
|
||||
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgvinoui')) {
|
||||
typeLabel = 'TGV inOUI';
|
||||
} else if (opNorm === 'tgv') {
|
||||
typeLabel = 'TGV';
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
typeLabel = 'OUIGO Train Classique';
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
typeLabel = 'OUIGO';
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
typeLabel = 'MOBIGO';
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
typeLabel = 'FLUO';
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
typeLabel = 'NOMAD';
|
||||
} else if (opNorm.includes('remi')) {
|
||||
typeLabel = 'RÉMI';
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
typeLabel = 'ALÉOP';
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
typeLabel = 'BREIZHGO';
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
typeLabel = 'HAUTS-DE-FRANCE';
|
||||
} else if (opNorm.includes('lio')) {
|
||||
typeLabel = 'LIÒ';
|
||||
} else if (opNorm.includes('zou')) {
|
||||
typeLabel = 'ZOU !';
|
||||
} else if (opNorm.includes('intercites') || opNorm === 'ic') {
|
||||
typeLabel = 'INTERCITÉS';
|
||||
} else if (opNorm.includes('ice')) {
|
||||
typeLabel = 'ICE';
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
typeLabel = 'EUROSTAR';
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
typeLabel = 'TGV Lyria';
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
typeLabel = 'AUTOCAR';
|
||||
} else if (opNorm.includes('ter')) {
|
||||
typeLabel = 'TER';
|
||||
}
|
||||
|
||||
// Spécificité régionale Bourgogne-Franche-Comté : TER commençant par 89xxxx ou 84xxxx -> MOBIGO
|
||||
if (typeLabel === 'TER' && (numTrain.startsWith('89') || numTrain.startsWith('84'))) {
|
||||
typeLabel = 'MOBIGO';
|
||||
if (!matchedLogo || matchedLogo.file === 'logo-ter.svg' || matchedLogo.file === 'logo-sncf.svg') {
|
||||
matchedLogo = logoLookup.get('mobigo') || matchedLogo;
|
||||
}
|
||||
}
|
||||
|
||||
const logoFile = (matchedLogo && matchedLogo.file) || 'logo-sncf.svg';
|
||||
const logoSrc = `/type/${logoFile}`;
|
||||
|
||||
return { logoSrc, typeLabel, numTrain };
|
||||
}
|
||||
Reference in new issue
Block a user