Refonte complète et fidèle des afficheurs Classiques Départs et Arrivées selon les images

This commit is contained in:
mathis committed 2026-08-30 22:45:03 +02:00
1 parent 62f268e677
commit 326e3e7c8b
4 files changed
+406 -763

No files matched your search

@@ -1,6 +1,8 @@
/* ══════════════════════════════════════════════════════════════════ /* ══════════════════════════════════════════════════════════════════
Fontes Achemine AFFICHEUR CLASSIQUES ARRIVÉES (Image 2)
Résolution standard 1920x1080 - 10 Lignes (108px/ligne)
══════════════════════════════════════════════════════════════════ */ ══════════════════════════════════════════════════════════════════ */
@font-face { @font-face {
font-family: 'Achemine'; font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype'); src: url('/public/fonts/achemine_regular.ttf') format('truetype');
@@ -23,400 +25,210 @@
font-display: swap; font-display: swap;
} }
/* ══════════════════════════════════════════════════════════════════ /* Conteneur 1920x1080 */
ANIMATIONS
══════════════════════════════════════════════════════════════════ */
/* Info cause perturbation — slide-in de droite vers la gauche */
@keyframes infoWrapperGrow {
from { max-width: 0; }
to { max-width: 576px; }
}
@keyframes infoSlideIn {
from { transform: translateX(100%); opacity: 0; }
40% { opacity: 1; }
to { transform: translateX(0%); opacity: 1; }
}
/* ══════════════════════════════════════════════════════════════════
CONTENEUR PRINCIPAL
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__container { .aff-classiques-arrivees__container {
/* Couleurs inspirées du vrai afficheur SNCF */ --green-odd: #23742f;
--yellow: #FFCC00; --green-even: #144e1c;
--blue-bg: #1e4a30; --yellow: #ffc700;
--blue-odd: #216b3e; --white: #ffffff;
--blue-even: #16552a; --sidebar-green: #29b851;
--text-white: #ffffff;
--text-muted: #7EE0A0;
position: relative; position: relative;
width: 100%; width: 1920px;
height: 100%; height: 1080px;
max-width: 1920px;
max-height: 1080px;
overflow: hidden; overflow: hidden;
background: var(--blue-bg); background: var(--green-even);
font-family: "Achemine", "Arial Narrow", Arial, sans-serif; font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* ══════════════════════════════════════════════════════════════════
WATERMARK "arrivées" — pivotée à 90°, fixée à droite
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__watermark {
position: absolute;
right: -97px;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
transform-origin: center;
/* Taille géante, très visible comme dans l'image */
font-size: 248px;
color: #216b3e;
font-weight: 900;
letter-spacing: 0.05em;
pointer-events: none;
user-select: none; user-select: none;
z-index: 1; box-sizing: border-box;
white-space: nowrap;
opacity: 0.3; /* Opacité diminuée */
} }
/* ══════════════════════════════════════════════════════════════════ /* Watermark / Texte vertical "arrivées" sur la droite */
LISTE STATIQUE — 10 lignes sur exactement 1080px .aff-classiques-arrivees__sidebar {
══════════════════════════════════════════════════════════════════ */ position: absolute;
top: 0;
right: 0;
width: 250px;
height: 1080px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: none;
}
.aff-classiques-arrivees__sidebar-text {
color: var(--sidebar-green);
font-size: 200px;
font-weight: 900;
letter-spacing: -2px;
line-height: 1;
transform: rotate(90deg);
transform-origin: center center;
white-space: nowrap;
font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
/* Liste des 10 lignes */
.aff-classiques-arrivees__list { .aff-classiques-arrivees__list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
height: 1080px; height: 1080px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
z-index: 2;
} }
/* ══════════════════════════════════════════════════════════════════ /* Chaque ligne (108px de haut) */
MODE DÉFILEMENT — conteneur + track animé
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__scroll-container {
height: 1080px;
overflow: hidden;
position: relative;
z-index: 2;
}
/* Le track contient 2× les données pour une boucle seamless */
.aff-classiques-arrivees__scroll-track {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
/* ══════════════════════════════════════════════════════════════════
ROW — chaque ligne occupe 1/10 de 1080px
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__row { .aff-classiques-arrivees__row {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 0 0 108px;
height: 108px; height: 108px;
min-height: 0; min-height: 108px;
padding: 0; max-height: 108px;
box-sizing: border-box; box-sizing: border-box;
padding-left: 20px;
padding-right: 240px; /* Espace réservé pour le texte vertical arrivées */
position: relative;
z-index: 2;
overflow: hidden; overflow: hidden;
} }
/* Alternance de tons bleus (comme l'image) */ .aff-classiques-arrivees__row:nth-child(odd) {
.aff-classiques-arrivees__row:nth-child(odd) { background: var(--blue-odd); } background: var(--green-odd);
.aff-classiques-arrivees__row:nth-child(even) { background: var(--blue-even); }
.aff-classiques-arrivees__row--empty {
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--text-white);
font-size: 32px;
text-align: center;
} }
/* ══════════════════════════════════════════════════════════════════ .aff-classiques-arrivees__row:nth-child(even) {
COL 1 — Logo opérateur (~9 % largeur) background: var(--green-even);
══════════════════════════════════════════════════════════════════ */ }
.aff-classiques-arrivees__row--empty {
opacity: 0.9;
}
/* Colonne 1 : Logo opérateur (blanc) */
.aff-classiques-arrivees__col-logo { .aff-classiques-arrivees__col-logo {
width: 173px; width: 105px;
flex-shrink: 0; min-width: 105px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
padding: 0 8px;
box-sizing: border-box;
}
.aff-classiques-arrivees__logo-box {
width: 100%;
/* Logo prend ~65% de la hauteur de la ligne */
height: 65px;
display: flex;
align-items: center;
justify-content: center;
} }
.aff-classiques-arrivees__op-logo { .aff-classiques-arrivees__op-logo {
max-width: 100%; max-width: 88px;
max-height: 100%; max-height: 52px;
object-fit: contain; object-fit: contain;
filter: brightness(0) invert(1);
} }
.aff-classiques-arrivees__logo-text { /* Colonne 2 : Type de train + Numéro */
color: var(--text-white);
font-weight: 700;
font-size: 22px;
text-align: center;
word-break: break-word;
line-height: 1.1;
}
/* ══════════════════════════════════════════════════════════════════
COL 2 — Type + Numéro OU Statut (~11 % largeur)
Alterne toutes les 4 secondes
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__col-meta { .aff-classiques-arrivees__col-meta {
width: 211px; width: 185px;
flex-shrink: 0; min-width: 185px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
justify-content: center; justify-content: center;
align-items: flex-start;
padding-left: 12px;
height: 100%; height: 100%;
padding: 0 10px 0 0;
box-sizing: border-box; box-sizing: border-box;
gap: 1px;
overflow: hidden;
text-align: center;
} }
/* Vue normale — type (ex: "MOBIGO") */
.aff-classiques-arrivees__train-type { .aff-classiques-arrivees__train-type {
font-size: 24px; font-size: 26px;
color: var(--text-white); font-weight: 700;
font-weight: 600; color: var(--white);
line-height: 1.3; line-height: 1.1;
text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
} }
/* Vue normale — numéro de sillon (ex: "894251") */
.aff-classiques-arrivees__train-number { .aff-classiques-arrivees__train-number {
font-size: 24px; font-size: 26px;
color: var(--text-white);
font-weight: 400;
line-height: 1.3;
white-space: nowrap;
}
/* Vue statut — à l'heure */
.aff-classiques-arrivees__status-ontime {
font-size: 24px;
color: var(--text-white);
font-weight: 400;
line-height: 1.3;
white-space: nowrap;
}
/* Vue statut — retardé */
.aff-classiques-arrivees__status-delayed {
display: flex;
flex-direction: column;
gap: 0;
line-height: 1.15;
}
.aff-classiques-arrivees__status-label {
font-size: 24px;
color: var(--yellow);
font-weight: 500;
white-space: nowrap;
}
.aff-classiques-arrivees__status-value {
font-size: 24px;
color: var(--yellow);
font-weight: 700; font-weight: 700;
color: var(--white);
line-height: 1.1;
margin-top: 4px;
white-space: nowrap; white-space: nowrap;
} }
/* Vue statut — supprimé */ /* Colonne 3 : Heure (Grande, jaune vif) */
.aff-classiques-arrivees__status-suppressed {
font-size: 24px;
color: var(--yellow);
font-weight: 700;
white-space: nowrap;
}
/* ══════════════════════════════════════════════════════════════════
COL 3 — Heure (~15 % largeur)
Grande, jaune vif (#FFCC00), bold — comme dans l'image
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__col-time { .aff-classiques-arrivees__col-time {
width: 288px; width: 290px;
flex-shrink: 0; min-width: 290px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
padding-left: 18px;
height: 100%; height: 100%;
padding: 0 6px 0 0;
box-sizing: border-box; box-sizing: border-box;
} }
.aff-classiques-arrivees__time { .aff-classiques-arrivees__time {
/* Jaune vif exactement comme dans la vidéo/image */
color: var(--yellow); color: var(--yellow);
font-size: 80px;
font-weight: 700; font-weight: 700;
/* Grande taille — occupe ~65% de la hauteur de ligne */
font-size: 70px;
line-height: 1; line-height: 1;
letter-spacing: -1px;
white-space: nowrap; white-space: nowrap;
font-family: "Achemine", sans-serif;
} }
/* Colonne 4 : Provenance (Gare d'origine) */
/* ══════════════════════════════════════════════════════════════════ .aff-classiques-arrivees__col-destination {
COL 4 — Origine + Info cause (flex:1 — prend tout le reste)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__col-origin {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; justify-content: flex-start;
padding-left: 10px;
padding-right: 16px;
height: 100%; height: 100%;
padding-right: 10px; overflow: hidden;
box-sizing: border-box;
} }
/* Texte destination — blanc, bold, très grand */
.aff-classiques-arrivees__destination { .aff-classiques-arrivees__destination {
color: var(--text-white); color: var(--white);
font-weight: 700; font-size: 74px;
font-size: 70px; font-weight: 600;
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 1 1 0; font-family: "Achemine", sans-serif;
min-width: 0;
} }
/* ─── Wrapper animé cause perturbation ────────────────────────── */ /* Colonne 5 : Quai / Voie */
.aff-classiques-arrivees__info-wrapper {
flex-shrink: 0;
overflow: hidden;
max-width: 0;
animation: infoWrapperGrow 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.aff-classiques-arrivees__info-inner {
display: flex;
align-items: center;
transform: translateX(100%);
animation: infoSlideIn 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
white-space: nowrap;
}
.aff-classiques-arrivees__separator {
color: var(--text-white);
font-size: 49px;
font-weight: 200;
line-height: 1;
flex-shrink: 0;
padding: 0 17px;
}
.aff-classiques-arrivees__info-text {
color: var(--text-white);
font-size: 21px;
font-weight: 400;
white-space: normal;
overflow: hidden;
line-height: 1.25;
max-width: 422px;
}
/* ══════════════════════════════════════════════════════════════════
COL 5 — Quai OU icône bus (~6.5 % largeur)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__col-platform { .aff-classiques-arrivees__col-platform {
width: 125px; width: 90px;
flex-shrink: 0; min-width: 90px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
box-sizing: border-box;
padding: 5px 8px;
} }
/* Boîte quai — bordure blanche */
.aff-classiques-arrivees__platform-box { .aff-classiques-arrivees__platform-box {
width: 65px; width: 58px;
height: 65px; height: 58px;
border: 2.5px solid rgba(255, 255, 255, 0.88); border: 3.5px solid var(--white);
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 7px;
background: transparent; background: transparent;
box-sizing: border-box; box-sizing: border-box;
} }
.aff-classiques-arrivees__platform-text { .aff-classiques-arrivees__platform-text {
color: var(--text-white); color: var(--white);
font-size: 44px;
font-weight: 700; font-weight: 700;
font-size: 49px;
line-height: 1; line-height: 1;
font-family: "Achemine", sans-serif;
} }
/* ══════════════════════════════════════════════════════════════════
HORLOGE — superposée en bas à droite (z-index élevé)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-arrivees__footer {
position: absolute;
right: 23px;
bottom: 9px;
z-index: 30;
pointer-events: none;
}
.aff-classiques-arrivees__clock-container {
display: flex;
align-items: center;
}
.aff-classiques-arrivees__clock-box {
border: 2.5px solid rgba(255, 255, 255, 0.88);
background: rgba(0, 51, 102, 0.93);
border-radius: 8px;
padding: 4px 21px;
display: flex;
align-items: baseline;
gap: 10px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.65);
}
.aff-classiques-arrivees__clock-main {
color: var(--text-white);
font-weight: 800;
font-size: 48px;
line-height: 1;
}
.aff-classiques-arrivees__clock-seconds {
color: var(--yellow);
font-weight: 800;
font-size: 29px;
line-height: 1;
}
/* masqué */
.aff-classiques-arrivees__station-box { display: none; }
@@ -1,7 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import './ClassiquesArrivees.css'; import './ClassiquesArrivees.css';
import supabase from '../../../lib/supabase'; import supabase from '../../../lib/supabase';
import Logo from '../../../components/Logo';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
@@ -49,8 +48,71 @@ const isSubstitution = (h) => {
return Boolean(subFlag || subStr || typeMatch || commentMatch); return Boolean(subFlag || subStr || typeMatch || commentMatch);
}; };
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les const formatStationName = (name) => {
// informations via .train-meta dans la colonne .col-train-type) if (!name) return '';
let str = String(name).trim();
str = str.replace(/^Gare de\s+/i, '').replace(/^Gare d'\s*/i, '');
str = str.replace(/([a-zA-ZÀ-ÿ])-(sur|en|les|sous|au|aux)-([a-zA-ZÀ-ÿ])/gi, '$1-$2-$3');
str = str.replace(/-(Viotte|Bercy|Part[- ]Dieu|Perrache|Gare[- ]de[- ]Lyon|Saint[- ]Lazare|Austerlitz|Montparnasse|Est|Nord|Ville|Centre|Rive[- ]Droite|Rive[- ]Gauche|Chantiers|Matabiau|Saint[- ]Jean|Saint[- ]Charles|Flandres|Europe|TGV|Alésia|Saint[- ]Gervais)/gi, ' $1');
str = str.replace(/Lyon-Part-Dieu/gi, 'Lyon Part Dieu');
str = str.replace(/Paris-Gare-de-Lyon/gi, 'Paris Gare de Lyon');
str = str.replace(/Paris-Bercy/gi, 'Paris Bercy');
str = str.replace(/Besançon-Viotte/gi, 'Besançon Viotte');
str = str.replace(/Besancon-Viotte/gi, 'Besançon Viotte');
str = str.replace(/Les-Laumes-Alésia/gi, 'Les Laumes Alésia');
str = str.replace(/Les-Laumes-Alesia/gi, 'Les Laumes Alésia');
str = str.replace(/Auxerre-Saint-Gervais/gi, 'Auxerre Saint-Gervais');
str = str.replace(/Lyon-Perrache/gi, 'Lyon Perrache');
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();
};
function Clock(){ function Clock(){
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
@@ -1148,67 +1210,35 @@ export default function ClassiquesArrivees(){
const renderRow = (h, idx) => { const renderRow = (h, idx) => {
if (h._isEmpty) { if (h._isEmpty) {
return <li className="aff-classiques-arrivees__row" key={`empty-${idx}`} style={{ background: 'transparent' }}></li>; return <li className="aff-classiques-arrivees__row aff-classiques-arrivees__row--empty" key={`empty-${idx}`} style={{ opacity: 0 }}></li>;
} }
const horaire = h || {}; const horaire = h || {};
const op = horaire.type_train || horaire.op || ''; const { logoSrc, typeLabel, numTrain } = getTrainMeta(horaire);
const isCarTER = (op === 'Car TER' || op === 'Autocar'); const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || ''); const origin = horaire.gare_depart_nom || horaire.gare_depart || horaire.origine || '';
const origin = horaire.gare_depart_nom || ''; const formattedOrigin = formatStationName(origin);
const platform = (horaire._displayPlatform || '').toString().trim(); const rawPlatform = (horaire._displayPlatform || horaire.voie || horaire.quai || '').toString().trim();
const cleanPlatform = formatPlatform(rawPlatform);
const stationId = horaire.gare_arrivee_id || horaire.gare_id || horaire.destination_id || (gareIdParam || null); const stationId = horaire.gare_arrivee_id || horaire.gare_id || horaire.destination_id || (gareIdParam || null);
const stationName = gareDisplay || horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination_name || null; 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 status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé'; const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); const showPlatform = !isSuppressed && Boolean(cleanPlatform);
let showPlatform = !isSuppressed && Boolean(platform);
const isSub = isSubstitution(horaire);
const infoText = isSub
? (horaire.motif_substitution && horaire.motif_substitution !== 'TRAVAUX' && horaire.motif_substitution !== 'Travaux.' ? horaire.motif_substitution : 'Autocar cour de la gare Plateforme 14')
: (status && status.causeDetails ? status.causeDetails : '');
const trainNumber = horaire.numero_train || horaire.numero || horaire.number || '';
return ( return (
<li className="aff-classiques-arrivees__row" key={idx}> <li className="aff-classiques-arrivees__row" key={idx}>
{/* COL 1 : Logo opérateur */} {/* COL 1 : Logo opérateur */}
<div className="aff-classiques-arrivees__col-logo"> <div className="aff-classiques-arrivees__col-logo">
{isCarTER ? ( <img src={logoSrc} alt={typeLabel} className="aff-classiques-arrivees__op-logo" />
<div className="aff-classiques-arrivees__logo-box">
<img src="/type/sncf-voyageurs-logo.png" alt="SNCF Voyageurs" className="aff-classiques-arrivees__op-logo" />
</div>
) : (
<Logo
op={op}
classNameBox="aff-classiques-arrivees__logo-box"
classNameImg="aff-classiques-arrivees__op-logo"
classNameText="aff-classiques-arrivees__logo-text"
/>
)}
</div> </div>
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */} {/* COL 2 : Type + Numéro */}
<div className="aff-classiques-arrivees__col-meta"> <div className="aff-classiques-arrivees__col-meta">
{showStatus ? ( <div className="aff-classiques-arrivees__train-type">{typeLabel}</div>
isSuppressed ? ( <div className="aff-classiques-arrivees__train-number">{numTrain}</div>
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
) : isDelayed ? (
<div className="aff-classiques-arrivees__status-delayed">
<div className="aff-classiques-arrivees__status-label">retard</div>
<div className="aff-classiques-arrivees__status-value">{status.delayMinutes} min.</div>
</div>
) : (
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
)
) : (
<>
<div className="aff-classiques-arrivees__train-type">{op}</div>
<div className="aff-classiques-arrivees__train-number">{trainNumber}</div>
</>
)}
</div> </div>
{/* COL 3 : Heure (grande, jaune) */} {/* COL 3 : Heure (grande, jaune) */}
@@ -1216,26 +1246,16 @@ export default function ClassiquesArrivees(){
<div className="aff-classiques-arrivees__time">{time}</div> <div className="aff-classiques-arrivees__time">{time}</div>
</div> </div>
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */} {/* COL 4 : Provenance */}
<div className="aff-classiques-arrivees__col-origin"> <div className="aff-classiques-arrivees__col-destination">
<MarqueeText text={origin} className="aff-classiques-arrivees__destination" /> <MarqueeText text={formattedOrigin} className="aff-classiques-arrivees__destination" />
{showInfoCause && infoText ? (
<div className="aff-classiques-arrivees__info-wrapper">
<div className="aff-classiques-arrivees__info-inner">
<div className="aff-classiques-arrivees__separator">|</div>
<div className="aff-classiques-arrivees__info-text">{infoText}</div>
</div>
</div>
) : null}
</div> </div>
{/* COL 5 : Quai OU icône bus */} {/* COL 5 : Quai / Voie */}
<div className="aff-classiques-arrivees__col-platform"> <div className="aff-classiques-arrivees__col-platform">
{isSub || isCarTER ? ( {showPlatform ? (
<img src="/type/track-car.svg" alt="Car" style={{ width: '70px', height: '70px' }} />
) : showPlatform ? (
<div className="aff-classiques-arrivees__platform-box"> <div className="aff-classiques-arrivees__platform-box">
<div className="aff-classiques-arrivees__platform-text">{platform}</div> <div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
</div> </div>
) : null} ) : null}
</div> </div>
@@ -1244,20 +1264,25 @@ export default function ClassiquesArrivees(){
); );
}; };
const displayList = [...horaires.slice(0, 10)];
while (displayList.length < 10) {
displayList.push({ _isEmpty: true });
}
return ( return (
<div className="aff-classiques-arrivees__container"> <div className="aff-classiques-arrivees__container">
{/* Watermark "départs" — diagonal droite */} {/* Watermark vertical "arrivées" sur la droite */}
<div className="aff-classiques-arrivees__watermark">arrivées</div> <div className="aff-classiques-arrivees__sidebar" aria-hidden>
<div className="aff-classiques-arrivees__sidebar-text">arrivées</div>
</div>
{defilementMode ? ( {defilementMode ? (
/* ── MODE DÉFILEMENT : marquee vertical infini ── */ /* ── MODE DÉFILEMENT : marquee vertical infini ── */
<div className="aff-classiques-arrivees__scroll-container"> <div className="aff-classiques-arrivees__scroll-container">
{horaires.length === 0 ? ( {horaires.length === 0 ? (
<ul className="aff-classiques-arrivees__list"> <ul className="aff-classiques-arrivees__list">
<li className="aff-classiques-arrivees__row aff-classiques-arrivees__row--empty"> {displayList.map((h, idx) => renderRow(h, idx))}
<div>Aucun horaire disponible.</div>
</li>
</ul> </ul>
) : ( ) : (
<ul <ul
@@ -1270,26 +1295,10 @@ export default function ClassiquesArrivees(){
) : ( ) : (
/* ── MODE NORMAL : 10 lignes statiques ── */ /* ── MODE NORMAL : 10 lignes statiques ── */
<ul className="aff-classiques-arrivees__list"> <ul className="aff-classiques-arrivees__list">
{horaires.length === 0 ? ( {displayList.map((h, idx) => renderRow(h, idx))}
<li className="aff-classiques-arrivees__row aff-classiques-arrivees__row--empty" key="empty">
<div>Aucun horaire disponible.</div>
<div style={{fontSize:'0.8rem', opacity:0.8}}>lignes brutes renvoyées : {rawDebug.count}</div>
{rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? (
<pre style={{maxHeight:160, overflow:'auto', fontSize:'0.75rem', marginTop:8, background:'#111', color:'#fff', padding:8}}>
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
</pre>
) : null}
</li>
) : horaires.map((h, idx) => renderRow(h, idx))}
</ul> </ul>
)} )}
{/* Horloge — superposée en bas à droite */}
<div className="aff-classiques-arrivees__footer" aria-hidden>
<Clock />
</div>
</div> </div>
) )
} }
@@ -1,6 +1,8 @@
/* ══════════════════════════════════════════════════════════════════ /* ══════════════════════════════════════════════════════════════════
Fontes Achemine AFFICHEUR CLASSIQUES DÉPARTS (Image 1)
Résolution standard 1920x1080 - 10 Lignes (108px/ligne)
══════════════════════════════════════════════════════════════════ */ ══════════════════════════════════════════════════════════════════ */
@font-face { @font-face {
font-family: 'Achemine'; font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype'); src: url('/public/fonts/achemine_regular.ttf') format('truetype');
@@ -23,400 +25,211 @@
font-display: swap; font-display: swap;
} }
/* ══════════════════════════════════════════════════════════════════ /* Conteneur 1920x1080 */
ANIMATIONS
══════════════════════════════════════════════════════════════════ */
/* Info cause perturbation — slide-in de droite vers la gauche */
@keyframes infoWrapperGrow {
from { max-width: 0; }
to { max-width: 576px; }
}
@keyframes infoSlideIn {
from { transform: translateX(100%); opacity: 0; }
40% { opacity: 1; }
to { transform: translateX(0%); opacity: 1; }
}
/* ══════════════════════════════════════════════════════════════════
CONTENEUR PRINCIPAL
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__container { .aff-classiques-departs__container {
/* Couleurs inspirées du vrai afficheur SNCF */ --blue-odd: #004b93;
--yellow: #FFCC00; --blue-even: #003066;
--blue-bg: #003a79; --yellow: #ffc700;
--blue-odd: #0163ab; --white: #ffffff;
--blue-even: #003a79; --sidebar-blue: #1084d7;
--text-white: #ffffff;
--text-muted: #7EB8E0;
position: relative; position: relative;
width: 100%; width: 1920px;
height: 100%; height: 1080px;
max-width: 1920px;
max-height: 1080px;
overflow: hidden; overflow: hidden;
background: var(--blue-bg); background: var(--blue-even);
font-family: "Achemine", "Arial Narrow", Arial, sans-serif; font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* ══════════════════════════════════════════════════════════════════
WATERMARK "départs" — pivotée à 90°, fixée à droite
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__watermark {
position: absolute;
right: -97px;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
transform-origin: center;
/* Taille géante, très visible comme dans l'image */
font-size: 248px;
color: #1389D2;
font-weight: 900;
letter-spacing: 0.05em;
pointer-events: none;
user-select: none; user-select: none;
z-index: 1; box-sizing: border-box;
white-space: nowrap;
opacity: 0.3; /* Opacité diminuée */
} }
/* ══════════════════════════════════════════════════════════════════ /* Watermark / Texte vertical "départs" sur la droite */
LISTE STATIQUE — 10 lignes sur exactement 1080px .aff-classiques-departs__sidebar {
══════════════════════════════════════════════════════════════════ */ position: absolute;
top: 0;
right: 0;
width: 250px;
height: 1080px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: none;
}
.aff-classiques-departs__sidebar-text {
color: var(--sidebar-blue);
font-size: 200px;
font-weight: 900;
letter-spacing: -2px;
line-height: 1;
transform: rotate(90deg);
transform-origin: center center;
white-space: nowrap;
font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
/* Liste des 10 lignes */
.aff-classiques-departs__list { .aff-classiques-departs__list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
height: 1080px; height: 1080px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
z-index: 2;
} }
/* ══════════════════════════════════════════════════════════════════ /* Chaque ligne (108px de haut) */
MODE DÉFILEMENT — conteneur + track animé
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__scroll-container {
height: 1080px;
overflow: hidden;
position: relative;
z-index: 2;
}
/* Le track contient 2× les données pour une boucle seamless */
.aff-classiques-departs__scroll-track {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
/* ══════════════════════════════════════════════════════════════════
ROW — chaque ligne occupe 1/10 de 1080px
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__row { .aff-classiques-departs__row {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 0 0 108px;
height: 108px; height: 108px;
min-height: 0; min-height: 108px;
padding: 0; max-height: 108px;
box-sizing: border-box; box-sizing: border-box;
padding-left: 20px;
padding-right: 240px; /* Espace réservé pour le texte vertical départs */
position: relative;
z-index: 2;
overflow: hidden; overflow: hidden;
} }
/* Alternance de tons bleus (comme l'image) */ .aff-classiques-departs__row:nth-child(odd) {
.aff-classiques-departs__row:nth-child(odd) { background: var(--blue-odd); } background: var(--blue-odd);
.aff-classiques-departs__row:nth-child(even) { background: var(--blue-even); }
.aff-classiques-departs__row--empty {
flex-direction: column;
justify-content: center;
align-items: center;
color: var(--text-white);
font-size: 32px;
text-align: center;
} }
/* ══════════════════════════════════════════════════════════════════ .aff-classiques-departs__row:nth-child(even) {
COL 1 — Logo opérateur (~9 % largeur) background: var(--blue-even);
══════════════════════════════════════════════════════════════════ */ }
.aff-classiques-departs__row--empty {
opacity: 0.9;
}
/* Colonne 1 : Logo opérateur (blanc) */
.aff-classiques-departs__col-logo { .aff-classiques-departs__col-logo {
width: 173px; width: 105px;
flex-shrink: 0; min-width: 105px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
padding: 0 8px;
box-sizing: border-box;
}
.aff-classiques-departs__logo-box {
width: 100%;
/* Logo prend ~65% de la hauteur de la ligne */
height: 65px;
display: flex;
align-items: center;
justify-content: center;
} }
.aff-classiques-departs__op-logo { .aff-classiques-departs__op-logo {
max-width: 100%; max-width: 88px;
max-height: 100%; max-height: 52px;
object-fit: contain; object-fit: contain;
filter: brightness(0) invert(1);
} }
.aff-classiques-departs__logo-text { /* Colonne 2 : Type de train + Numéro */
color: var(--text-white);
font-weight: 700;
font-size: 22px;
text-align: center;
word-break: break-word;
line-height: 1.1;
}
/* ══════════════════════════════════════════════════════════════════
COL 2 — Type + Numéro OU Statut (~11 % largeur)
Alterne toutes les 4 secondes
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__col-meta { .aff-classiques-departs__col-meta {
width: 211px; width: 185px;
flex-shrink: 0; min-width: 185px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
justify-content: center; justify-content: center;
align-items: flex-start;
padding-left: 12px;
height: 100%; height: 100%;
padding: 0 10px 0 0;
box-sizing: border-box; box-sizing: border-box;
gap: 1px;
overflow: hidden;
text-align: center;
} }
/* Vue normale — type (ex: "MOBIGO") */
.aff-classiques-departs__train-type { .aff-classiques-departs__train-type {
font-size: 24px; font-size: 26px;
color: var(--text-white); font-weight: 700;
font-weight: 600; color: var(--white);
line-height: 1.3; line-height: 1.1;
text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
} }
/* Vue normale — numéro de sillon (ex: "894251") */
.aff-classiques-departs__train-number { .aff-classiques-departs__train-number {
font-size: 24px; font-size: 26px;
color: var(--text-white);
font-weight: 400;
line-height: 1.3;
white-space: nowrap;
}
/* Vue statut — à l'heure */
.aff-classiques-departs__status-ontime {
font-size: 24px;
color: var(--text-white);
font-weight: 400;
line-height: 1.3;
white-space: nowrap;
}
/* Vue statut — retardé */
.aff-classiques-departs__status-delayed {
display: flex;
flex-direction: column;
gap: 0;
line-height: 1.15;
}
.aff-classiques-departs__status-label {
font-size: 24px;
color: var(--yellow);
font-weight: 500;
white-space: nowrap;
}
.aff-classiques-departs__status-value {
font-size: 24px;
color: var(--yellow);
font-weight: 700; font-weight: 700;
color: var(--white);
line-height: 1.1;
margin-top: 4px;
white-space: nowrap; white-space: nowrap;
} }
/* Vue statut — supprimé */ /* Colonne 3 : Heure (Grande, jaune vif) */
.aff-classiques-departs__status-suppressed {
font-size: 24px;
color: var(--yellow);
font-weight: 700;
white-space: nowrap;
}
/* ══════════════════════════════════════════════════════════════════
COL 3 — Heure (~15 % largeur)
Grande, jaune vif (#FFCC00), bold — comme dans l'image
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__col-time { .aff-classiques-departs__col-time {
width: 288px; width: 290px;
flex-shrink: 0; min-width: 290px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
padding-left: 18px;
height: 100%; height: 100%;
padding: 0 6px 0 0;
box-sizing: border-box; box-sizing: border-box;
} }
.aff-classiques-departs__time { .aff-classiques-departs__time {
/* Jaune vif exactement comme dans la vidéo/image */
color: var(--yellow); color: var(--yellow);
font-size: 80px;
font-weight: 700; font-weight: 700;
/* Grande taille — occupe ~65% de la hauteur de ligne */
font-size: 70px;
line-height: 1; line-height: 1;
letter-spacing: -1px;
white-space: nowrap; white-space: nowrap;
font-family: "Achemine", sans-serif;
} }
/* ══════════════════════════════════════════════════════════════════ /* Colonne 4 : Destination */
COL 4 — Destination + Info cause (flex:1 — prend tout le reste)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__col-destination { .aff-classiques-departs__col-destination {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; justify-content: flex-start;
padding-left: 10px;
padding-right: 16px;
height: 100%; height: 100%;
padding-right: 10px; overflow: hidden;
box-sizing: border-box;
} }
/* Texte destination — blanc, bold, très grand */
.aff-classiques-departs__destination { .aff-classiques-departs__destination {
color: var(--text-white); color: var(--white);
font-weight: 700; font-size: 74px;
font-size: 70px; font-weight: 600;
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 1 1 0; font-family: "Achemine", sans-serif;
min-width: 0;
} }
/* ─── Wrapper animé cause perturbation ────────────────────────── */ /* Colonne 5 : Quai / Voie */
.aff-classiques-departs__info-wrapper {
flex-shrink: 0;
overflow: hidden;
max-width: 0;
animation: infoWrapperGrow 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.aff-classiques-departs__info-inner {
display: flex;
align-items: center;
transform: translateX(100%);
animation: infoSlideIn 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
white-space: nowrap;
}
.aff-classiques-departs__separator {
color: var(--text-white);
font-size: 49px;
font-weight: 200;
line-height: 1;
flex-shrink: 0;
padding: 0 17px;
}
.aff-classiques-departs__info-text {
color: var(--text-white);
font-size: 21px;
font-weight: 400;
white-space: normal;
overflow: hidden;
line-height: 1.25;
max-width: 422px;
}
/* ══════════════════════════════════════════════════════════════════
COL 5 — Quai OU icône bus (~6.5 % largeur)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__col-platform { .aff-classiques-departs__col-platform {
width: 125px; width: 90px;
flex-shrink: 0; min-width: 90px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
box-sizing: border-box;
padding: 5px 8px;
} }
/* Boîte quai — bordure blanche */
.aff-classiques-departs__platform-box { .aff-classiques-departs__platform-box {
width: 65px; width: 58px;
height: 65px; height: 58px;
border: 2.5px solid rgba(255, 255, 255, 0.88); border: 3.5px solid var(--white);
border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 7px;
background: transparent; background: transparent;
box-sizing: border-box; box-sizing: border-box;
} }
.aff-classiques-departs__platform-text { .aff-classiques-departs__platform-text {
color: var(--text-white); color: var(--white);
font-size: 44px;
font-weight: 700; font-weight: 700;
font-size: 49px;
line-height: 1; line-height: 1;
font-family: "Achemine", sans-serif;
} }
/* ══════════════════════════════════════════════════════════════════
HORLOGE — superposée en bas à droite (z-index élevé)
══════════════════════════════════════════════════════════════════ */
.aff-classiques-departs__footer {
position: absolute;
right: 23px;
bottom: 9px;
z-index: 30;
pointer-events: none;
}
.aff-classiques-departs__clock-container {
display: flex;
align-items: center;
}
.aff-classiques-departs__clock-box {
border: 2.5px solid rgba(255, 255, 255, 0.88);
background: rgba(0, 51, 102, 0.93);
border-radius: 8px;
padding: 4px 21px;
display: flex;
align-items: baseline;
gap: 10px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.65);
}
.aff-classiques-departs__clock-main {
color: var(--text-white);
font-weight: 800;
font-size: 48px;
line-height: 1;
}
.aff-classiques-departs__clock-seconds {
color: var(--yellow);
font-weight: 800;
font-size: 29px;
line-height: 1;
}
/* masqué */
.aff-classiques-departs__station-box { display: none; }
@@ -1,7 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import './ClassiquesDeparts.css'; import './ClassiquesDeparts.css';
import supabase from '../../../lib/supabase'; import supabase from '../../../lib/supabase';
import Logo from '../../../components/Logo';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
@@ -49,8 +48,71 @@ const isSubstitution = (h) => {
return Boolean(subFlag || subStr || typeMatch || commentMatch); return Boolean(subFlag || subStr || typeMatch || commentMatch);
}; };
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les const formatStationName = (name) => {
// informations via .train-meta dans la colonne .col-train-type) if (!name) return '';
let str = String(name).trim();
str = str.replace(/^Gare de\s+/i, '').replace(/^Gare d'\s*/i, '');
str = str.replace(/([a-zA-ZÀ-ÿ])-(sur|en|les|sous|au|aux)-([a-zA-ZÀ-ÿ])/gi, '$1-$2-$3');
str = str.replace(/-(Viotte|Bercy|Part[- ]Dieu|Perrache|Gare[- ]de[- ]Lyon|Saint[- ]Lazare|Austerlitz|Montparnasse|Est|Nord|Ville|Centre|Rive[- ]Droite|Rive[- ]Gauche|Chantiers|Matabiau|Saint[- ]Jean|Saint[- ]Charles|Flandres|Europe|TGV|Alésia|Saint[- ]Gervais)/gi, ' $1');
str = str.replace(/Lyon-Part-Dieu/gi, 'Lyon Part Dieu');
str = str.replace(/Paris-Gare-de-Lyon/gi, 'Paris Gare de Lyon');
str = str.replace(/Paris-Bercy/gi, 'Paris Bercy');
str = str.replace(/Besançon-Viotte/gi, 'Besançon Viotte');
str = str.replace(/Besancon-Viotte/gi, 'Besançon Viotte');
str = str.replace(/Les-Laumes-Alésia/gi, 'Les Laumes Alésia');
str = str.replace(/Les-Laumes-Alesia/gi, 'Les Laumes Alésia');
str = str.replace(/Auxerre-Saint-Gervais/gi, 'Auxerre Saint-Gervais');
str = str.replace(/Lyon-Perrache/gi, 'Lyon Perrache');
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();
};
function Clock(){ function Clock(){
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
@@ -1145,67 +1207,35 @@ export default function ClassiquesDeparts(){
const renderRow = (h, idx) => { const renderRow = (h, idx) => {
if (h._isEmpty) { if (h._isEmpty) {
return <li className="aff-classiques-departs__row" key={`empty-${idx}`} style={{ background: 'transparent' }}></li>; return <li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key={`empty-${idx}`} style={{ opacity: 0 }}></li>;
} }
const horaire = h || {}; const horaire = h || {};
const op = horaire.type_train || horaire.op || ''; const { logoSrc, typeLabel, numTrain } = getTrainMeta(horaire);
const isCarTER = (op === 'Car TER' || op === 'Autocar'); const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.departure_time || horaire.time || horaire.heure || '');
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || ''); const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '';
const destination = horaire.gare_arrivee_nom || ''; const formattedDestination = formatStationName(destination);
const platform = (horaire._displayPlatform || '').toString().trim(); const rawPlatform = (horaire._displayPlatform || horaire.voie || horaire.quai || '').toString().trim();
const cleanPlatform = formatPlatform(rawPlatform);
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null); const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
const stationName = gareDisplay || horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null; 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 status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé'; const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); const showPlatform = !isSuppressed && Boolean(cleanPlatform);
let showPlatform = !isSuppressed && Boolean(platform);
const isSub = isSubstitution(horaire);
const infoText = isSub
? (horaire.motif_substitution && horaire.motif_substitution !== 'TRAVAUX' && horaire.motif_substitution !== 'Travaux.' ? horaire.motif_substitution : 'Autocar cour de la gare Plateforme 14')
: (status && status.causeDetails ? status.causeDetails : '');
const trainNumber = horaire.numero_train || horaire.numero || horaire.number || '';
return ( return (
<li className="aff-classiques-departs__row" key={idx}> <li className="aff-classiques-departs__row" key={idx}>
{/* COL 1 : Logo opérateur */} {/* COL 1 : Logo opérateur */}
<div className="aff-classiques-departs__col-logo"> <div className="aff-classiques-departs__col-logo">
{isCarTER ? ( <img src={logoSrc} alt={typeLabel} className="aff-classiques-departs__op-logo" />
<div className="aff-classiques-departs__logo-box">
<img src="/type/sncf-voyageurs-logo.png" alt="SNCF Voyageurs" className="aff-classiques-departs__op-logo" />
</div>
) : (
<Logo
op={op}
classNameBox="aff-classiques-departs__logo-box"
classNameImg="aff-classiques-departs__op-logo"
classNameText="aff-classiques-departs__logo-text"
/>
)}
</div> </div>
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */} {/* COL 2 : Type + Numéro */}
<div className="aff-classiques-departs__col-meta"> <div className="aff-classiques-departs__col-meta">
{showStatus ? ( <div className="aff-classiques-departs__train-type">{typeLabel}</div>
isSuppressed ? ( <div className="aff-classiques-departs__train-number">{numTrain}</div>
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
) : isDelayed ? (
<div className="aff-classiques-departs__status-delayed">
<div className="aff-classiques-departs__status-label">retard</div>
<div className="aff-classiques-departs__status-value">{status.delayMinutes} min.</div>
</div>
) : (
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
)
) : (
<>
<div className="aff-classiques-departs__train-type">{op}</div>
<div className="aff-classiques-departs__train-number">{trainNumber}</div>
</>
)}
</div> </div>
{/* COL 3 : Heure (grande, jaune) */} {/* COL 3 : Heure (grande, jaune) */}
@@ -1213,26 +1243,16 @@ export default function ClassiquesDeparts(){
<div className="aff-classiques-departs__time">{time}</div> <div className="aff-classiques-departs__time">{time}</div>
</div> </div>
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */} {/* COL 4 : Destination */}
<div className="aff-classiques-departs__col-destination"> <div className="aff-classiques-departs__col-destination">
<MarqueeText text={destination} className="aff-classiques-departs__destination" /> <MarqueeText text={formattedDestination} className="aff-classiques-departs__destination" />
{showInfoCause && infoText ? (
<div className="aff-classiques-departs__info-wrapper">
<div className="aff-classiques-departs__info-inner">
<div className="aff-classiques-departs__separator">|</div>
<div className="aff-classiques-departs__info-text">{infoText}</div>
</div>
</div>
) : null}
</div> </div>
{/* COL 5 : Quai OU icône bus */} {/* COL 5 : Quai / Voie */}
<div className="aff-classiques-departs__col-platform"> <div className="aff-classiques-departs__col-platform">
{isSub || isCarTER ? ( {showPlatform ? (
<img src="/type/track-car.svg" alt="Car" style={{ width: '70px', height: '70px' }} />
) : showPlatform ? (
<div className="aff-classiques-departs__platform-box"> <div className="aff-classiques-departs__platform-box">
<div className="aff-classiques-departs__platform-text">{platform}</div> <div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
</div> </div>
) : null} ) : null}
</div> </div>
@@ -1241,20 +1261,25 @@ export default function ClassiquesDeparts(){
); );
}; };
const displayList = [...horaires.slice(0, 10)];
while (displayList.length < 10) {
displayList.push({ _isEmpty: true });
}
return ( return (
<div className="aff-classiques-departs__container"> <div className="aff-classiques-departs__container">
{/* Watermark "départs" — diagonal droite */} {/* Watermark vertical "départs" sur la droite */}
<div className="aff-classiques-departs__watermark">départs</div> <div className="aff-classiques-departs__sidebar" aria-hidden>
<div className="aff-classiques-departs__sidebar-text">départs</div>
</div>
{defilementMode ? ( {defilementMode ? (
/* ── MODE DÉFILEMENT : marquee vertical infini ── */ /* ── MODE DÉFILEMENT : marquee vertical infini ── */
<div className="aff-classiques-departs__scroll-container"> <div className="aff-classiques-departs__scroll-container">
{horaires.length === 0 ? ( {horaires.length === 0 ? (
<ul className="aff-classiques-departs__list"> <ul className="aff-classiques-departs__list">
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty"> {displayList.map((h, idx) => renderRow(h, idx))}
<div>Aucun horaire disponible.</div>
</li>
</ul> </ul>
) : ( ) : (
<ul <ul
@@ -1267,26 +1292,10 @@ export default function ClassiquesDeparts(){
) : ( ) : (
/* ── MODE NORMAL : 10 lignes statiques ── */ /* ── MODE NORMAL : 10 lignes statiques ── */
<ul className="aff-classiques-departs__list"> <ul className="aff-classiques-departs__list">
{horaires.length === 0 ? ( {displayList.map((h, idx) => renderRow(h, idx))}
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key="empty">
<div>Aucun horaire disponible.</div>
<div style={{fontSize:'0.8rem', opacity:0.8}}>lignes brutes renvoyées : {rawDebug.count}</div>
{rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? (
<pre style={{maxHeight:160, overflow:'auto', fontSize:'0.75rem', marginTop:8, background:'#111', color:'#fff', padding:8}}>
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
</pre>
) : null}
</li>
) : horaires.map((h, idx) => renderRow(h, idx))}
</ul> </ul>
)} )}
{/* Horloge — superposée en bas à droite */}
<div className="aff-classiques-departs__footer" aria-hidden>
<Clock />
</div>
</div> </div>
) )
} }