From 326e3e7c8b86fd9efe94c14f79be9afa2760e726 Mon Sep 17 00:00:00 2001 From: mathis Date: Sun, 30 Aug 2026 22:45:03 +0200 Subject: [PATCH] =?UTF-8?q?Refonte=20compl=C3=A8te=20et=20fid=C3=A8le=20de?= =?UTF-8?q?s=20afficheurs=20Classiques=20D=C3=A9parts=20et=20Arriv=C3=A9es?= =?UTF-8?q?=20selon=20les=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classiques/ClassiquesArrivees.css | 408 +++++------------- .../classiques/ClassiquesArrivees.js | 181 ++++---- .../classiques/ClassiquesDeparts.css | 405 +++++------------ .../classiques/ClassiquesDeparts.js | 179 ++++---- 4 files changed, 408 insertions(+), 765 deletions(-) diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.css b/src/pages/afficheurs/classiques/ClassiquesArrivees.css index d5b15dc..5eb3f37 100644 --- a/src/pages/afficheurs/classiques/ClassiquesArrivees.css +++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.css @@ -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; } \ No newline at end of file + font-family: "Achemine", sans-serif; +} \ No newline at end of file diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js index 0e17f85..b6250f9 100644 --- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js +++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js @@ -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
  • ; + return
  • ; } 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 (
  • {/* COL 1 : Logo opérateur */}
    - {isCarTER ? ( -
    - SNCF Voyageurs -
    - ) : ( - - )} + {typeLabel}
    - {/* COL 2 : Type + Numéro (4s) OU Statut (4s) */} + {/* COL 2 : Type + Numéro */}
    - {showStatus ? ( - isSuppressed ? ( -
    supprimé
    - ) : isDelayed ? ( -
    -
    retard
    -
    {status.delayMinutes} min.
    -
    - ) : ( -
    à l'heure
    - ) - ) : ( - <> -
    {op}
    -
    {trainNumber}
    - - )} +
    {typeLabel}
    +
    {numTrain}
    {/* COL 3 : Heure (grande, jaune) */} @@ -1216,26 +1246,16 @@ export default function ClassiquesArrivees(){
    {time}
    - {/* COL 4 : Destination + Info cause (slide-in 5s/5s) */} -
    - - {showInfoCause && infoText ? ( -
    -
    -
    |
    -
    {infoText}
    -
    -
    - ) : null} + {/* COL 4 : Provenance */} +
    +
    - {/* COL 5 : Quai OU icône bus */} + {/* COL 5 : Quai / Voie */}
    - {isSub || isCarTER ? ( - Car - ) : showPlatform ? ( + {showPlatform ? (
    -
    {platform}
    +
    {cleanPlatform}
    ) : null}
    @@ -1244,20 +1264,25 @@ export default function ClassiquesArrivees(){ ); }; + const displayList = [...horaires.slice(0, 10)]; + while (displayList.length < 10) { + displayList.push({ _isEmpty: true }); + } + return (
    - {/* Watermark "départs" — diagonal droite */} -
    arrivées
    + {/* Watermark vertical "arrivées" sur la droite */} +
    +
    arrivées
    +
    {defilementMode ? ( /* ── MODE DÉFILEMENT : marquee vertical infini ── */
    {horaires.length === 0 ? (
      -
    • -
      Aucun horaire disponible.
      -
    • + {displayList.map((h, idx) => renderRow(h, idx))}
    ) : (
      - {horaires.length === 0 ? ( -
    • -
      Aucun horaire disponible.
      -
      lignes brutes renvoyées : {rawDebug.count}
      - {rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? ( -
      -				  {JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}
      -				  {JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
      -				
      - ) : null} -
    • - ) : horaires.map((h, idx) => renderRow(h, idx))} + {displayList.map((h, idx) => renderRow(h, idx))}
    )} - {/* Horloge — superposée en bas à droite */} -
    - -
    -
    ) } \ No newline at end of file diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.css b/src/pages/afficheurs/classiques/ClassiquesDeparts.css index cb27193..8e76d73 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.css +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.css @@ -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; } \ No newline at end of file + font-family: "Achemine", sans-serif; +} \ No newline at end of file diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js index 2321f9a..b040a64 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js @@ -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
  • ; + return
  • ; } 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 (
  • {/* COL 1 : Logo opérateur */}
    - {isCarTER ? ( -
    - SNCF Voyageurs -
    - ) : ( - - )} + {typeLabel}
    - {/* COL 2 : Type + Numéro (4s) OU Statut (4s) */} + {/* COL 2 : Type + Numéro */}
    - {showStatus ? ( - isSuppressed ? ( -
    supprimé
    - ) : isDelayed ? ( -
    -
    retard
    -
    {status.delayMinutes} min.
    -
    - ) : ( -
    à l'heure
    - ) - ) : ( - <> -
    {op}
    -
    {trainNumber}
    - - )} +
    {typeLabel}
    +
    {numTrain}
    {/* COL 3 : Heure (grande, jaune) */} @@ -1213,26 +1243,16 @@ export default function ClassiquesDeparts(){
    {time}
    - {/* COL 4 : Destination + Info cause (slide-in 5s/5s) */} + {/* COL 4 : Destination */}
    - - {showInfoCause && infoText ? ( -
    -
    -
    |
    -
    {infoText}
    -
    -
    - ) : null} +
    - {/* COL 5 : Quai OU icône bus */} + {/* COL 5 : Quai / Voie */}
    - {isSub || isCarTER ? ( - Car - ) : showPlatform ? ( + {showPlatform ? (
    -
    {platform}
    +
    {cleanPlatform}
    ) : null}
    @@ -1241,20 +1261,25 @@ export default function ClassiquesDeparts(){ ); }; + const displayList = [...horaires.slice(0, 10)]; + while (displayList.length < 10) { + displayList.push({ _isEmpty: true }); + } + return (
    - {/* Watermark "départs" — diagonal droite */} -
    départs
    + {/* Watermark vertical "départs" sur la droite */} +
    +
    départs
    +
    {defilementMode ? ( /* ── MODE DÉFILEMENT : marquee vertical infini ── */
    {horaires.length === 0 ? (
      -
    • -
      Aucun horaire disponible.
      -
    • + {displayList.map((h, idx) => renderRow(h, idx))}
    ) : (
      - {horaires.length === 0 ? ( -
    • -
      Aucun horaire disponible.
      -
      lignes brutes renvoyées : {rawDebug.count}
      - {rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? ( -
      -				  {JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}
      -				  {JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
      -				
      - ) : null} -
    • - ) : horaires.map((h, idx) => renderRow(h, idx))} + {displayList.map((h, idx) => renderRow(h, idx))}
    )} - {/* Horloge — superposée en bas à droite */} -
    - -
    -
    ) } \ No newline at end of file