Refonte complète et fidèle des afficheurs Classiques Départs et Arrivées selon les images
This commit is contained in:
1 parent
62f268e677
commit
326e3e7c8b
4 files changed
+408
-765
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-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
@@ -23,400 +25,210 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Conteneur 1920x1080 */
|
||||
.aff-classiques-arrivees__container {
|
||||
/* Couleurs inspirées du vrai afficheur SNCF */
|
||||
--yellow: #FFCC00;
|
||||
--blue-bg: #1e4a30;
|
||||
--blue-odd: #216b3e;
|
||||
--blue-even: #16552a;
|
||||
--text-white: #ffffff;
|
||||
--text-muted: #7EE0A0;
|
||||
--green-odd: #23742f;
|
||||
--green-even: #144e1c;
|
||||
--yellow: #ffc700;
|
||||
--white: #ffffff;
|
||||
--sidebar-green: #29b851;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
max-width: 1920px;
|
||||
max-height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--blue-bg);
|
||||
font-family: "Achemine", "Arial Narrow", Arial, 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;
|
||||
background: var(--green-even);
|
||||
font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
opacity: 0.3; /* Opacité diminuée */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
LISTE STATIQUE — 10 lignes sur exactement 1080px
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Watermark / Texte vertical "arrivées" sur la droite */
|
||||
.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 {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Chaque ligne (108px de haut) */
|
||||
.aff-classiques-arrivees__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 108px;
|
||||
height: 108px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Alternance de tons bleus (comme l'image) */
|
||||
.aff-classiques-arrivees__row:nth-child(odd) { background: var(--blue-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(odd) {
|
||||
background: var(--green-odd);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 1 — Logo opérateur (~9 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__row:nth-child(even) {
|
||||
background: var(--green-even);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__row--empty {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
.aff-classiques-arrivees__col-logo {
|
||||
width: 173px;
|
||||
flex-shrink: 0;
|
||||
width: 105px;
|
||||
min-width: 105px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 88px;
|
||||
max-height: 52px;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__logo-text {
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 2 : Type de train + Numéro */
|
||||
.aff-classiques-arrivees__col-meta {
|
||||
width: 211px;
|
||||
flex-shrink: 0;
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-left: 12px;
|
||||
height: 100%;
|
||||
padding: 0 10px 0 0;
|
||||
box-sizing: border-box;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Vue normale — type (ex: "MOBIGO") */
|
||||
.aff-classiques-arrivees__train-type {
|
||||
font-size: 24px;
|
||||
color: var(--text-white);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Vue normale — numéro de sillon (ex: "894251") */
|
||||
.aff-classiques-arrivees__train-number {
|
||||
font-size: 24px;
|
||||
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-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — supprimé */
|
||||
.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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-arrivees__col-time {
|
||||
width: 288px;
|
||||
flex-shrink: 0;
|
||||
width: 290px;
|
||||
min-width: 290px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-left: 18px;
|
||||
height: 100%;
|
||||
padding: 0 6px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__time {
|
||||
/* Jaune vif exactement comme dans la vidéo/image */
|
||||
color: var(--yellow);
|
||||
font-size: 80px;
|
||||
font-weight: 700;
|
||||
/* Grande taille — occupe ~65% de la hauteur de ligne */
|
||||
font-size: 70px;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 4 — Origine + Info cause (flex:1 — prend tout le reste)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-origin {
|
||||
/* Colonne 4 : Provenance (Gare d'origine) */
|
||||
.aff-classiques-arrivees__col-destination {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
padding-left: 10px;
|
||||
padding-right: 16px;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Texte destination — blanc, bold, très grand */
|
||||
.aff-classiques-arrivees__destination {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 70px;
|
||||
color: var(--white);
|
||||
font-size: 74px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* ─── Wrapper animé cause perturbation ────────────────────────── */
|
||||
.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)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 5 : Quai / Voie */
|
||||
.aff-classiques-arrivees__col-platform {
|
||||
width: 125px;
|
||||
flex-shrink: 0;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
/* Boîte quai — bordure blanche */
|
||||
.aff-classiques-arrivees__platform-box {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border: 3.5px solid var(--white);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__platform-text {
|
||||
color: var(--text-white);
|
||||
color: var(--white);
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
font-size: 49px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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; }
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesArrivees.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import Logo from '../../../components/Logo';
|
||||
import MarqueeText from '../../../components/MarqueeText';
|
||||
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
@@ -49,8 +48,71 @@ const isSubstitution = (h) => {
|
||||
return Boolean(subFlag || subStr || typeMatch || commentMatch);
|
||||
};
|
||||
|
||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||
// informations via .train-meta dans la colonne .col-train-type)
|
||||
const formatStationName = (name) => {
|
||||
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(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -1148,67 +1210,35 @@ export default function ClassiquesArrivees(){
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
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 op = horaire.type_train || horaire.op || '';
|
||||
const isCarTER = (op === 'Car TER' || op === 'Autocar');
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || '';
|
||||
const platform = (horaire._displayPlatform || '').toString().trim();
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainMeta(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);
|
||||
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 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é');
|
||||
|
||||
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 || '';
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-arrivees__row" key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-arrivees__col-logo">
|
||||
{isCarTER ? (
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-arrivees__op-logo" />
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */}
|
||||
{/* COL 2 : Type + Numéro */}
|
||||
<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">
|
||||
<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 className="aff-classiques-arrivees__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-arrivees__train-number">{numTrain}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
@@ -1216,26 +1246,16 @@ export default function ClassiquesArrivees(){
|
||||
<div className="aff-classiques-arrivees__time">{time}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */}
|
||||
<div className="aff-classiques-arrivees__col-origin">
|
||||
<MarqueeText text={origin} 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}
|
||||
{/* COL 4 : Provenance */}
|
||||
<div className="aff-classiques-arrivees__col-destination">
|
||||
<MarqueeText text={formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
</div>
|
||||
|
||||
{/* COL 5 : Quai OU icône bus */}
|
||||
{/* COL 5 : Quai / Voie */}
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{isSub || isCarTER ? (
|
||||
<img src="/type/track-car.svg" alt="Car" style={{ width: '70px', height: '70px' }} />
|
||||
) : showPlatform ? (
|
||||
{showPlatform ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1244,20 +1264,25 @@ export default function ClassiquesArrivees(){
|
||||
);
|
||||
};
|
||||
|
||||
const displayList = [...horaires.slice(0, 10)];
|
||||
while (displayList.length < 10) {
|
||||
displayList.push({ _isEmpty: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-arrivees__container">
|
||||
|
||||
{/* Watermark "départs" — diagonal droite */}
|
||||
<div className="aff-classiques-arrivees__watermark">arrivées</div>
|
||||
{/* Watermark vertical "arrivées" sur la droite */}
|
||||
<div className="aff-classiques-arrivees__sidebar" aria-hidden>
|
||||
<div className="aff-classiques-arrivees__sidebar-text">arrivées</div>
|
||||
</div>
|
||||
|
||||
{defilementMode ? (
|
||||
/* ── MODE DÉFILEMENT : marquee vertical infini ── */
|
||||
<div className="aff-classiques-arrivees__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-arrivees__list">
|
||||
<li className="aff-classiques-arrivees__row aff-classiques-arrivees__row--empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
</li>
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
@@ -1270,26 +1295,10 @@ export default function ClassiquesArrivees(){
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
<ul className="aff-classiques-arrivees__list">
|
||||
{horaires.length === 0 ? (
|
||||
<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))}
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Horloge — superposée en bas à droite */}
|
||||
<div className="aff-classiques-arrivees__footer" aria-hidden>
|
||||
<Clock />
|
||||
</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-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
@@ -23,400 +25,211 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Conteneur 1920x1080 */
|
||||
.aff-classiques-departs__container {
|
||||
/* Couleurs inspirées du vrai afficheur SNCF */
|
||||
--yellow: #FFCC00;
|
||||
--blue-bg: #003a79;
|
||||
--blue-odd: #0163ab;
|
||||
--blue-even: #003a79;
|
||||
--text-white: #ffffff;
|
||||
--text-muted: #7EB8E0;
|
||||
--blue-odd: #004b93;
|
||||
--blue-even: #003066;
|
||||
--yellow: #ffc700;
|
||||
--white: #ffffff;
|
||||
--sidebar-blue: #1084d7;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
max-width: 1920px;
|
||||
max-height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--blue-bg);
|
||||
font-family: "Achemine", "Arial Narrow", Arial, 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;
|
||||
background: var(--blue-even);
|
||||
font-family: "Achemine", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
opacity: 0.3; /* Opacité diminuée */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
LISTE STATIQUE — 10 lignes sur exactement 1080px
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Watermark / Texte vertical "départs" sur la droite */
|
||||
.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 {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Chaque ligne (108px de haut) */
|
||||
.aff-classiques-departs__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 108px;
|
||||
height: 108px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
min-height: 108px;
|
||||
max-height: 108px;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Alternance de tons bleus (comme l'image) */
|
||||
.aff-classiques-departs__row:nth-child(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(odd) {
|
||||
background: var(--blue-odd);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 1 — Logo opérateur (~9 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__row:nth-child(even) {
|
||||
background: var(--blue-even);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row--empty {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
.aff-classiques-departs__col-logo {
|
||||
width: 173px;
|
||||
flex-shrink: 0;
|
||||
width: 105px;
|
||||
min-width: 105px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 88px;
|
||||
max-height: 52px;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__logo-text {
|
||||
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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 2 : Type de train + Numéro */
|
||||
.aff-classiques-departs__col-meta {
|
||||
width: 211px;
|
||||
flex-shrink: 0;
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-left: 12px;
|
||||
height: 100%;
|
||||
padding: 0 10px 0 0;
|
||||
box-sizing: border-box;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Vue normale — type (ex: "MOBIGO") */
|
||||
.aff-classiques-departs__train-type {
|
||||
font-size: 24px;
|
||||
color: var(--text-white);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Vue normale — numéro de sillon (ex: "894251") */
|
||||
.aff-classiques-departs__train-number {
|
||||
font-size: 24px;
|
||||
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-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — supprimé */
|
||||
.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
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-departs__col-time {
|
||||
width: 288px;
|
||||
flex-shrink: 0;
|
||||
width: 290px;
|
||||
min-width: 290px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-left: 18px;
|
||||
height: 100%;
|
||||
padding: 0 6px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__time {
|
||||
/* Jaune vif exactement comme dans la vidéo/image */
|
||||
color: var(--yellow);
|
||||
font-size: 80px;
|
||||
font-weight: 700;
|
||||
/* Grande taille — occupe ~65% de la hauteur de ligne */
|
||||
font-size: 70px;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 4 — Destination + Info cause (flex:1 — prend tout le reste)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 4 : Destination */
|
||||
.aff-classiques-departs__col-destination {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
padding-left: 10px;
|
||||
padding-right: 16px;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Texte destination — blanc, bold, très grand */
|
||||
.aff-classiques-departs__destination {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 70px;
|
||||
color: var(--white);
|
||||
font-size: 74px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* ─── Wrapper animé cause perturbation ────────────────────────── */
|
||||
.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)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
/* Colonne 5 : Quai / Voie */
|
||||
.aff-classiques-departs__col-platform {
|
||||
width: 125px;
|
||||
flex-shrink: 0;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
/* Boîte quai — bordure blanche */
|
||||
.aff-classiques-departs__platform-box {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border: 3.5px solid var(--white);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__platform-text {
|
||||
color: var(--text-white);
|
||||
color: var(--white);
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
font-size: 49px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
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; }
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import Logo from '../../../components/Logo';
|
||||
import MarqueeText from '../../../components/MarqueeText';
|
||||
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
@@ -49,8 +48,71 @@ const isSubstitution = (h) => {
|
||||
return Boolean(subFlag || subStr || typeMatch || commentMatch);
|
||||
};
|
||||
|
||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||
// informations via .train-meta dans la colonne .col-train-type)
|
||||
const formatStationName = (name) => {
|
||||
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(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -1145,67 +1207,35 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
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 op = horaire.type_train || horaire.op || '';
|
||||
const isCarTER = (op === 'Car TER' || op === 'Autocar');
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
const platform = (horaire._displayPlatform || '').toString().trim();
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainMeta(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);
|
||||
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 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é');
|
||||
|
||||
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 || '';
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-departs__row" key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
{isCarTER ? (
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<img src={logoSrc} alt={typeLabel} className="aff-classiques-departs__op-logo" />
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */}
|
||||
{/* COL 2 : Type + Numéro */}
|
||||
<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">
|
||||
<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 className="aff-classiques-departs__train-type">{typeLabel}</div>
|
||||
<div className="aff-classiques-departs__train-number">{numTrain}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
@@ -1213,26 +1243,16 @@ export default function ClassiquesDeparts(){
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */}
|
||||
{/* COL 4 : Destination */}
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={destination} 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}
|
||||
<MarqueeText text={formattedDestination} className="aff-classiques-departs__destination" />
|
||||
</div>
|
||||
|
||||
{/* COL 5 : Quai OU icône bus */}
|
||||
{/* COL 5 : Quai / Voie */}
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{isSub || isCarTER ? (
|
||||
<img src="/type/track-car.svg" alt="Car" style={{ width: '70px', height: '70px' }} />
|
||||
) : showPlatform ? (
|
||||
{showPlatform ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1241,20 +1261,25 @@ export default function ClassiquesDeparts(){
|
||||
);
|
||||
};
|
||||
|
||||
const displayList = [...horaires.slice(0, 10)];
|
||||
while (displayList.length < 10) {
|
||||
displayList.push({ _isEmpty: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__container">
|
||||
|
||||
{/* Watermark "départs" — diagonal droite */}
|
||||
<div className="aff-classiques-departs__watermark">départs</div>
|
||||
{/* Watermark vertical "départs" sur la droite */}
|
||||
<div className="aff-classiques-departs__sidebar" aria-hidden>
|
||||
<div className="aff-classiques-departs__sidebar-text">départs</div>
|
||||
</div>
|
||||
|
||||
{defilementMode ? (
|
||||
/* ── MODE DÉFILEMENT : marquee vertical infini ── */
|
||||
<div className="aff-classiques-departs__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-departs__list">
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
</li>
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
@@ -1267,26 +1292,10 @@ export default function ClassiquesDeparts(){
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{horaires.length === 0 ? (
|
||||
<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))}
|
||||
{displayList.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Horloge — superposée en bas à droite */}
|
||||
<div className="aff-classiques-departs__footer" aria-hidden>
|
||||
<Clock />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user