RELEASE V3.0.0 : Voici les modifications apportées :

-> Refonte complète des afficheurs AFL départ et arrivée (+ Quais)
-> (ALPHA) : Implémentation des afficheurs EVA (Départ et arrivée uniquement pour l'instant).
This commit is contained in:
mathis committed 2026-08-28 04:07:19 +02:00
1 parent 19868a451f
commit 6dfd798dd2
16 files changed
+6155 -2008

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzg3NjY3MjMxLCJleHAiOjE5NDUzNDcyMzF9.uD7YUlOFIhK_Jqbra94o5-N5zdrzKezoCxV067RJi78
+23
View File
@@ -25,6 +25,29 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<style>
@font-face {
font-family: 'Achemine';
src: url('%PUBLIC_URL%/fonts/achemine_regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('%PUBLIC_URL%/fonts/achemine_bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('%PUBLIC_URL%/fonts/achemine_italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
+8
View File
@@ -14,6 +14,8 @@ import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
import AFLArriveesQuais from './pages/afficheurs/afl/AFLArriveesQuais';
import EVAArrivees from './pages/afficheurs/eva/EVAArrivees';
import EVADeparts from './pages/afficheurs/eva/EVADeparts';
import SelectAfficheur from './pages/SelectAfficheur';
applyPolyfills().then(() => defineCustomElements(window));
@@ -141,12 +143,18 @@ function App() {
if (route.startsWith('/afficheurs/departs/afl')) {
return <AFLDeparts />;
}
if (route.startsWith('/afficheurs/departs/eva')) {
return <EVADeparts />;
}
if (route.startsWith('/afficheurs/arrivees/afl/quais')) {
return <AFLArriveesQuais />;
}
if (route.startsWith('/afficheurs/arrivees/afl')) {
return <AFLArrivees />;
}
if (route.startsWith('/afficheurs/arrivees/eva')) {
return <EVAArrivees />;
}
if (route.startsWith('/afficheurs/arrivees/classiques')) {
return <ClassiquesArrivees />;
}
+9 -3
View File
@@ -1,7 +1,7 @@
import React, { useRef, useState, useEffect } from 'react';
import './MarqueeText.css';
const MarqueeText = ({ text, className }) => {
const MarqueeText = ({ text, className, style }) => {
const containerRef = useRef(null);
const textRef = useRef(null);
const [isOverflowing, setIsOverflowing] = useState(false);
@@ -18,13 +18,19 @@ const MarqueeText = ({ text, className }) => {
}, [text]);
return (
<div className={`marquee-container ${className || ''}`} ref={containerRef} style={{ overflow: 'hidden', whiteSpace: 'nowrap', width: '100%', flex: '1 1 0' }}>
<div className={`marquee-container ${className || ''}`} ref={containerRef} style={{ ...style, overflow: 'hidden', whiteSpace: 'nowrap', width: '100%', flex: '1 1 0' }}>
<div
className="marquee-inner-text"
ref={textRef}
style={{
display: 'inline-block',
animation: isOverflowing ? 'marquee-scroll 10s linear infinite' : 'none',
paddingLeft: isOverflowing ? '100%' : '0'
paddingLeft: isOverflowing ? '100%' : '0',
fontSize: 'inherit',
fontWeight: 'inherit',
color: 'inherit',
lineHeight: 'inherit',
letterSpacing: 'inherit'
}}
>
{text}
+382 -285
View File
@@ -1,382 +1,479 @@
/* Déclaration des fontes Achemine (dans public/fonts/) */
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
/* ==========================================================================
AFL ARRIVEES (GRANDS ARRIVEES SNCF) - CSS
========================================================================== */
/* Reset & Global Container */
.afl-arr-container {
font-family: 'Achemine', 'Segoe UI', Arial, sans-serif !important;
height: 100vh !important;
width: 100vw !important;
display: flex !important;
flex-direction: column !important;
background-color: #173e31 !important; /* Vert sombre SNCF Arrivées */
overflow: hidden !important;
color: #ffffff !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
user-select: none !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
.afl-arrivees__container {
font-family: 'Achemine', Arial, 'Helvetica Neue', Helvetica, sans-serif;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
background-color: #1e4a30; /* Vivid dark blue background */
overflow: hidden;
color: white;
margin: 0;
padding: 0;
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
z-index: 10000;
/* ==========================================================================
HEADER (10vh)
========================================================================== */
.afl-arr-header {
height: 10vh !important;
min-height: 10vh !important;
max-height: 10vh !important;
background-color: #337862 !important; /* Vert moyen SNCF Arrivées */
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 2.5vw !important;
border-bottom: 2px solid #ffffff !important;
box-sizing: border-box !important;
z-index: 10 !important;
}
.afl-arrivees__header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #216b3e; /* Bright blue header */
padding: 0 2vw;
height: 10vh;
flex-shrink: 0;
.afl-arr-header-left {
display: flex !important;
align-items: center !important;
gap: 1.5vw !important;
}
.afl-arrivees__header-left {
display: flex;
align-items: center;
gap: 1.5vw;
width: 30vw;
.afl-arr-header-title {
font-size: 6.2vh !important;
font-weight: 700 !important;
color: #ffffff !important;
letter-spacing: -0.05vw !important;
line-height: 1 !important;
}
.afl-arrivees__title {
font-size: 5vh;
font-weight: 700;
.afl-arr-header-center {
display: flex !important;
align-items: center !important;
gap: 1.2vw !important;
}
.afl-arrivees__header-center {
display: flex;
align-items: center;
justify-content: center;
gap: 1vw;
flex: 1;
.afl-arr-page-dot {
width: 4.5vh !important;
height: 4.5vh !important;
border-radius: 50% !important;
background-color: rgba(255, 255, 255, 0.3) !important;
color: #ffffff !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 3.0vh !important;
font-weight: 700 !important;
}
.afl-arrivees__page-dot {
width: 4vh;
height: 4vh;
border-radius: 50%;
background-color: #1e4a30; /* Dark blue for inactive */
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2vh;
font-weight: 700;
.afl-arr-page-dot--active {
background-color: #3dba8b !important; /* Vert clair émeraude actif */
color: #173e31 !important;
}
.afl-arrivees__page-dot--active {
background-color: #4caf50; /* Cyan for active */
.afl-arr-header-right {
display: flex !important;
align-items: center !important;
}
.afl-arrivees__header-right {
display: flex;
justify-content: flex-end;
width: 30vw;
/* Horloge */
.afl-arr-clock-container {
display: flex !important;
align-items: baseline !important;
font-weight: 700 !important;
color: #ffffff !important;
}
.afl-arrivees__header-right .aff-classiques-departs__clock-box {
background: transparent;
border: none;
box-shadow: none;
.afl-arr-clock-main {
font-size: 6.2vh !important;
line-height: 1 !important;
letter-spacing: -0.05vw !important;
}
.afl-arrivees__header-right .aff-classiques-departs__clock-main {
font-size: 4.5vh;
font-weight: 600;
color: #ffffff;
}
.afl-arrivees__header-right .aff-classiques-departs__clock-seconds {
display: none;
/* ==========================================================================
TRAIN ROWS LIST
========================================================================== */
.afl-arr-list {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
background-color: #173e31 !important;
}
.afl-arrivees__list {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
.afl-arr-row {
display: flex !important;
width: 100vw !important;
border-bottom: 2px solid #ffffff !important;
padding-left: 2.5vw !important;
padding-right: 1.5vw !important;
background-color: #173e31 !important;
box-sizing: border-box !important;
}
.afl-arrivees__row {
display: flex;
align-items: flex-start;
padding: 2.5vh 2vw 0 2vw;
position: relative;
height: 17.5vh;
box-sizing: border-box;
/* --------------------------------------------------------------------------
MODE ETENDU (20vh)
-------------------------------------------------------------------------- */
.afl-arr-row--detailed {
height: 20vh !important;
min-height: 20vh !important;
max-height: 20vh !important;
align-items: stretch !important;
}
.afl-arrivees__col-time-status {
width: 16vw;
flex-shrink: 0;
display: flex;
flex-direction: column;
.afl-arr-row--detailed .afl-arr-col-time,
.afl-arr-row--detailed .afl-arr-col-train,
.afl-arr-row--detailed .afl-arr-col-dest {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
height: 100% !important;
}
.afl-arrivees__time {
font-size: 7vh;
font-weight: 700;
color: #fce205; /* Bright yellow */
line-height: 1;
letter-spacing: -2px;
/* --------------------------------------------------------------------------
MODE REDUIT (10vh) - CENTRAGE PARFAIT DE TOUS LES TEXTES
-------------------------------------------------------------------------- */
.afl-arr-row--compact {
height: 10vh !important;
min-height: 10vh !important;
max-height: 10vh !important;
align-items: center !important;
}
.afl-arrivees__time--suppressed {
color: #ff4444;
font-size: 4vh;
text-transform: uppercase;
.afl-arr-row--compact .afl-arr-col-time,
.afl-arr-row--compact .afl-arr-col-train,
.afl-arr-row--compact .afl-arr-col-dest,
.afl-arr-row--compact .afl-arr-col-voie {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
height: 100% !important;
}
.afl-arrivees__status-pill {
display: inline-flex;
align-items: center;
background-color: #216b3e; /* Blue background matching header */
border-radius: 3vh;
padding: 0.6vh 0.8vw;
margin-top: 1vh;
font-size: 2.5vh;
font-weight: 500;
color: white;
width: max-content;
/* ==========================================================================
COLUMNS LAYOUT
========================================================================== */
/* Colonne 1 : Heure & Badge Statut (Centrage synchronisé sur 13.5vw) */
.afl-arr-col-time {
width: 13.5vw !important;
min-width: 13.5vw !important;
max-width: 13.5vw !important;
margin-right: 2.5vw !important;
padding: 0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.afl-arrivees__status-pill svg {
margin-right: 0.5vw;
.afl-arr-row--detailed .afl-arr-col-time .afl-arr-top-row {
display: flex !important;
justify-content: center !important;
align-items: flex-end !important;
width: 100% !important;
}
.afl-arrivees__status-pill--delayed {
background-color: #ff9e43;
color: #111;
.afl-arr-row--detailed .afl-arr-col-time .afl-arr-bottom-row {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
}
.afl-arrivees__status-pill--suppressed {
background-color: #ff4c5b;
color: #111;
.afl-arr-row--detailed .afl-arr-col-time .afl-arr-text-time {
text-align: center !important;
}
.afl-arrivees__col-train-info {
width: 12vw;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding-top: 0.5vh;
align-items: flex-start; /* Left align type and number */
.afl-arr-row--compact .afl-arr-col-time {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 13.5vw !important;
min-width: 13.5vw !important;
max-width: 13.5vw !important;
margin-right: 2.5vw !important;
}
.afl-arrivees__op-name {
font-size: 3vh;
font-weight: 700;
line-height: 1.2;
.afl-arr-row--compact .afl-arr-col-time .afl-arr-text-time {
text-align: center !important;
}
.afl-arrivees__op-number {
font-size: 2.5vh;
font-weight: 400;
color: #a4d2b1;
margin-top: 0.5vh;
/* Colonne 2 : Type de train / Opérateur & Numéro */
.afl-arr-col-train {
width: 15vw !important;
min-width: 15vw !important;
max-width: 15vw !important;
margin-right: 2.5vw !important;
padding: 0 !important;
}
.afl-arrivees__col-destination {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 2vw;
padding-right: 2vw;
padding-top: 0;
overflow: hidden;
/* Colonne 3 : Provenance & Gares desservies */
.afl-arr-col-dest {
flex: 1 !important;
overflow: hidden !important;
padding-right: 1.5vw !important;
}
.afl-arrivees__dest-line {
display: flex;
align-items: center;
gap: 1vw;
margin-bottom: 0.5vh;
/* Colonne 4 : Voie */
.afl-arr-col-voie {
width: 9.5vw !important;
min-width: 9.5vw !important;
max-width: 9.5vw !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.afl-arrivees__destination-name {
font-size: 6vh;
font-weight: 700;
line-height: 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -1px;
/* ==========================================================================
PARTIE HAUTE (LIGNES ETENDUES)
========================================================================== */
.afl-arr-top-row {
height: 8.5vh !important;
display: flex !important;
align-items: flex-end !important;
width: 100% !important;
overflow: hidden !important;
}
.afl-arrivees__vias-wrapper {
display: flex;
align-items: center;
width: 100%;
/* Heure */
.afl-arr-text-time {
font-size: 8.0vh !important;
font-weight: 800 !important;
color: #ffff00 !important;
line-height: 1 !important;
letter-spacing: -0.08vw !important;
margin: 0 !important;
padding: 0 !important;
}
.afl-arrivees__vias-static {
font-size: 3.5vh;
font-weight: 400;
color: #a4d2b1;
white-space: pre;
.afl-arr-row--compact .afl-arr-text-time {
font-size: 8.0vh !important;
display: flex !important;
align-items: center !important;
}
.afl-arrivees__vias-container {
flex: 1;
overflow: hidden;
white-space: nowrap;
position: relative;
/* Opérateur */
.afl-arr-text-op {
font-size: 5.0vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
text-transform: uppercase !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.afl-arrivees__vias {
font-size: 3.5vh;
font-weight: 400;
color: #a4d2b1;
display: inline-block;
white-space: nowrap;
animation: afl-marquee 6.6s linear infinite;
padding-left: 100%;
/* Destination / Provenance */
.afl-arr-text-dest {
font-size: 8.0vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: -0.05vw !important;
white-space: nowrap !important;
}
@keyframes afl-marquee {
0% { transform: translate(0, 0); }
100% { transform: translate(-100%, 0); }
.afl-arr-row--compact .afl-arr-text-dest {
font-size: 7.5vh !important;
}
.afl-arrivees__col-platform {
width: 12vw;
flex-shrink: 0;
display: flex;
justify-content: flex-end;
padding-top: 0;
/* ==========================================================================
PARTIE BASSE (LIGNES ETENDUES)
========================================================================== */
.afl-arr-bottom-row {
height: 7.5vh !important;
display: flex !important;
align-items: center !important;
margin-top: 1.0vh !important;
width: 100% !important;
overflow: hidden !important;
}
.afl-arrivees__platform-box {
background-color: white;
border-radius: 0.8vh;
width: 10vw;
height: 12.5vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0.5vh 1vh rgba(0,0,0,0.2);
/* Numéro de train */
.afl-arr-text-num {
font-size: 4.8vh !important;
font-weight: 700 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: 0.05vw !important;
white-space: nowrap !important;
}
.afl-arrivees__platform-label {
color: #1e4a30; /* Matches background */
font-size: 2.8vh;
font-weight: 700;
line-height: 1.2;
.afl-arr-row--compact .afl-arr-text-num {
font-size: 5.2vh !important;
display: flex !important;
align-items: center !important;
}
.afl-arrivees__platform-value {
color: #1e4a30; /* Matches background */
font-size: 6.8vh;
font-weight: 700;
line-height: 1.1;
/* ==========================================================================
STATUS PILLS (DIMENSIONS EXACTES)
========================================================================== */
.afl-arr-pill {
display: inline-flex !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 0.8vw !important;
padding: 0.5vh 1.4vw 0.5vh 0.6vw !important;
border-radius: 3.0vh !important;
height: 5.8vh !important;
margin: 0 !important;
}
.afl-arrivees__separator-wrapper {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2vh;
display: flex;
align-items: center;
justify-content: center;
.afl-arr-pill-text {
font-size: 3.6vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
.afl-arrivees__separator {
position: absolute;
height: 0.4vh;
background-color: #16552a;
width: 96vw;
border-radius: 0.2vh;
.afl-arr-pill--ontime {
background-color: #337862 !important;
color: #ffffff !important;
}
.afl-arrivees__train-illustration {
position: absolute;
z-index: 2;
background-color: #1e4a30; /* matches background */
padding: 0 1vw;
.afl-arr-pill--delayed {
background-color: #f8c898 !important;
color: #111111 !important;
}
.afl-arrivees__train-illustration svg {
display: block;
.afl-arr-pill--suppressed {
background-color: #ff5c73 !important;
color: #111111 !important;
}
body:has(.afl-arrivees__container) {
margin: 0;
padding: 0;
overflow: hidden;
.afl-arr-icon {
width: 4.0vh !important;
height: 4.0vh !important;
flex-shrink: 0 !important;
}
.afl-arrivees__container * {
box-sizing: border-box;
/* ==========================================================================
GARES DESSERVIES (VIAS - 'VIA' FIXE VERT & DEFILEMENT DES GARES ULTRA-RAPIDE)
========================================================================== */
.afl-arr-vias-container {
display: flex !important;
align-items: baseline !important;
white-space: nowrap !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
.afl-arrivees__separator-wrapper {
position: relative;
width: 100%;
height: 10px;
display: flex;
align-items: center;
justify-content: center;
.afl-arr-vias-label {
font-size: 3.6vh !important;
font-weight: 700 !important;
color: #3dba8b !important; /* Vert menthe SNCF Arrivées */
margin-right: 0.5vw !important;
flex-shrink: 0 !important;
vertical-align: baseline !important;
}
.afl-arrivees__separator {
position: absolute;
height: 4px;
background-color: #16552a;
width: calc(100% - 48px);
border-radius: 2px;
.afl-arr-vias-scroll-wrapper {
flex: 1 !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
display: flex !important;
align-items: baseline !important;
}
.afl-arrivees__train-illustration {
position: absolute;
z-index: 2;
background-color: #01347d; /* match row background so it overlays the line */
padding: 0 10px;
.afl-arr-vias-inner {
display: inline-block !important;
white-space: nowrap !important;
}
.afl-arrivees__row--compact {
height: 10.5vh;
align-items: center;
padding-top: 0;
padding-bottom: 0;
.afl-arr-vias-inner--animating {
animation-name: afl-arr-vias-marquee !important;
animation-timing-function: linear !important;
animation-iteration-count: infinite !important;
padding-left: 20% !important;
}
.afl-arrivees__row--compact .afl-arrivees__platform-box {
height: 8vh;
@keyframes afl-arr-vias-marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
.afl-arrivees__row--compact .afl-arrivees__platform-label {
font-size: 1.8vh;
.afl-arr-vias-stop {
font-size: 5.5vh !important;
font-weight: 400 !important;
color: #ffffff !important;
line-height: 1 !important;
vertical-align: baseline !important;
}
.afl-arrivees__row--compact .afl-arrivees__platform-value {
font-size: 4vh;
.afl-arr-vias-chevron {
color: #3dba8b !important; /* Chevrons en vert menthe */
font-weight: 800 !important;
margin: 0 0.4vw !important;
font-size: 5.5vh !important;
vertical-align: baseline !important;
}
.afl-arrivees__row--compact .afl-arrivees__col-time-status,
.afl-arrivees__row--compact .afl-arrivees__col-train-info,
.afl-arrivees__row--compact .afl-arrivees__col-destination,
.afl-arrivees__row--compact .afl-arrivees__col-platform {
padding-top: 0;
/* ==========================================================================
VOIE BOX (BLANCHE / PROPORTIONS STRICTES)
========================================================================== */
.afl-arr-voie-box {
background-color: #ffffff !important;
border-radius: 1.8vh !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 0.2vh !important;
width: 8.5vw !important;
height: 15.2vh !important;
padding: 0.5vh 0 !important;
box-shadow: 0 0.4vh 1.0vh rgba(0, 0, 0, 0.25) !important;
}
.afl-arr-voie-label {
color: #173e31 !important;
font-size: 3.5vh !important;
font-weight: 600 !important;
line-height: 1 !important;
letter-spacing: -0.02vw !important;
margin: 0 !important;
}
.afl-arr-voie-value {
color: #173e31 !important;
font-size: 9.8vh !important;
font-weight: 800 !important;
line-height: 0.9 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.afl-arr-voie-box--bus {
justify-content: center !important;
align-items: center !important;
padding: 0 !important;
}
.afl-arr-voie-box-compact {
background-color: #ffffff !important;
border-radius: 1.0vh !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 6.2vw !important;
height: 7.6vh !important;
padding: 0 !important;
box-shadow: 0 0.2vh 0.5vh rgba(0, 0, 0, 0.25) !important;
}
+321 -239
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import './AFLArrivees.css';
import supabase from '../../../lib/supabase';
import { fetchAllRows } from '../../../lib/fetchAllRows';
@@ -25,8 +25,73 @@ const formatTimeNoSeconds = (v) => {
return String(v).slice(0,5).replace(':','h');
};
// (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 BusIcon = ({ size = '5.5vh', color = 'white', style = {} }) => (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={color}
style={{ flexShrink: 0, ...style }}
>
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
</svg>
);
const ViasMarquee = ({ viaNames }) => {
const containerRef = useRef(null);
const textRef = useRef(null);
const [isOverflowing, setIsOverflowing] = useState(false);
const [duration, setDuration] = useState(4);
useEffect(() => {
const check = () => {
if (containerRef.current && textRef.current) {
const cWidth = containerRef.current.clientWidth;
const tWidth = textRef.current.scrollWidth;
if (tWidth > cWidth) {
setIsOverflowing(true);
// Vitesse de défilement rapide (~280px/s)
setDuration(Math.max(1.0, tWidth / 280));
} else {
setIsOverflowing(false);
}
}
};
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, [viaNames]);
if (!viaNames || viaNames.length === 0) {
return (
<div className="afl-arr-vias-container">
<span className="afl-arr-vias-stop">Direct</span>
</div>
);
}
return (
<div className="afl-arr-vias-container">
<span className="afl-arr-vias-label">via</span>
<div className="afl-arr-vias-scroll-wrapper" ref={containerRef}>
<div
ref={textRef}
className={`afl-arr-vias-inner ${isOverflowing ? 'afl-arr-vias-inner--animating' : ''}`}
style={{
animationDuration: isOverflowing ? `${duration}s` : undefined
}}
>
{viaNames.map((v, i) => (
<React.Fragment key={i}>
{i > 0 && <span className="afl-arr-vias-chevron">&gt;</span>}
<span className="afl-arr-vias-stop">{v}</span>
</React.Fragment>
))}
</div>
</div>
</div>
);
};
function Clock(){
const [now, setNow] = useState(new Date());
@@ -37,14 +102,10 @@ function Clock(){
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
return (
<div className="aff-classiques-departs__clock-container" aria-hidden>
<div className="aff-classiques-departs__clock-box">
<div className="aff-classiques-departs__clock-main">{hh}:{mm}</div>
<div className="aff-classiques-departs__clock-seconds">{ss}</div>
</div>
<div className="afl-arr-clock-container" aria-hidden>
<span className="afl-arr-clock-main">{hh}:{mm}</span>
</div>
);
}
@@ -58,14 +119,17 @@ export default function AFLArrivees(){
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
const pageParam = params.get('page');
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
? Math.max(0, parseInt(pageParam, 10) - 1)
: null;
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
const debug = useCallback((...args) => { if (debugEnabled) console.debug(...args); }, [debugEnabled]);
// État pour alterner l'affichage toutes les 3 secondes
// eslint-disable-next-line no-unused-vars
const [showStatus, setShowStatus] = useState(false); // Gardé pour éviter des warnings, mais non utilisé pour basculer
// État pour l'heure actuelle, mise à jour chaque seconde pour la logique d'affichage du quai
// État pour l'heure actuelle
const [currentTime, setCurrentTime] = useState(new Date());
// Met à jour l'heure actuelle chaque seconde
@@ -76,15 +140,15 @@ export default function AFLArrivees(){
// Données depuis Supabase
const [horaires, setHoraires] = useState([]);
// eslint-disable-next-line no-unused-vars
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
// eslint-disable-next-line no-unused-vars
const [stationType, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setTravauxSubstitutions] = useState([]);
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
// debug des données brutes renvoyées par Supabase
const [, setRawDebug] = useState({ count: 0, sample: [] });
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -248,6 +312,7 @@ export default function AFLArrivees(){
}, []);
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
@@ -259,6 +324,7 @@ export default function AFLArrivees(){
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
// eslint-disable-next-line no-unused-vars
const thresholdForStationType = useCallback((type) => {
if (!type) return 30; // 30 minutes par défaut
const t = String(type).trim().toLowerCase();
@@ -560,11 +626,6 @@ export default function AFLArrivees(){
});
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
useEffect(() => {
const id = setInterval(() => setShowStatus(s => !s), 3000);
return () => clearInterval(id);
}, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
useEffect(() => {
let mounted = true;
@@ -940,8 +1001,8 @@ export default function AFLArrivees(){
// Debug: log first few annotated horaires
try { debug('annotated horaires sample', annotated.slice(0,6).map(a => ({ id: a.id, numero: a.numero_train || a.numero, _status: a._status, _displayTime: a._displayTime }))); } catch (e) {}
// afficher seulement les 9 premières lignes sur l'afficheur
setHoraires((annotated || []).slice(0, 9));
// Enregistrer tous les horaires (sans limitation à 9 pour alimenter toutes les pages)
setHoraires(annotated || []);
setPerturbations(filteredPerturb || []);
setTravaux(travauxData || []);
setTravauxSubstitutions(travauxSubstData || []);
@@ -965,14 +1026,14 @@ export default function AFLArrivees(){
// Helpers pour perturbations (memoized plus haut)
const [pageIndex, setPageIndex] = useState(0);
const [showAlternateStatus, setShowAlternateStatus] = useState(false);
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
const [alternateDelay, setAlternateDelay] = useState(false);
useEffect(() => {
const alternateInterval = setInterval(() => {
setShowAlternateStatus(prev => !prev);
const delayTimer = setInterval(() => {
setAlternateDelay(prev => !prev);
}, 2000);
return () => clearInterval(alternateInterval);
return () => clearInterval(delayTimer);
}, []);
const getPageBounds = useCallback((pIndex) => {
@@ -981,9 +1042,13 @@ export default function AFLArrivees(){
return { start, end: start + 8 };
}, []);
const totalPages = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
useEffect(() => {
if (lockedPageIndex !== null) {
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
return;
}
const pageInterval = setInterval(() => {
setPageIndex((prev) => {
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
@@ -992,241 +1057,258 @@ export default function AFLArrivees(){
});
}, 10000); // Change page every 10 seconds
return () => clearInterval(pageInterval);
}, [horaires.length]);
}, [horaires.length, lockedPageIndex, totalPages]);
const { start, end } = getPageBounds(pageIndex);
const displayedHoraires = horaires.slice(start, end);
const isPage1 = pageIndex === 0;
return (
<div className="afl-arrivees__container">
{/* HEADER */}
<div className="afl-arrivees__header">
<div className="afl-arrivees__header-left">
<div className="afl-arrivees__logo-wrapper">
<svg viewBox="0 0 24 24" width="4vh" height="4vh" fill="white">
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
</svg>
</div>
<div className="afl-arrivees__title">Arrivées</div>
</div>
<div className="afl-arr-container">
{/* HEADER */}
<header className="afl-arr-header">
<div className="afl-arr-header-left">
<svg width="4.5vh" height="4.5vh" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
{/* Porte de train verticale */}
<path d="M5 4v20M5 4h3M5 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
{/* Silhouette tête */}
<circle cx="10.5" cy="7" r="2.2" fill="white" />
{/* Silhouette corps + marche vers la droite */}
<path d="M8.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
{/* Chevrons >> */}
<path d="M20 8l4 6-4 6M15 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="afl-arr-header-title">Arrivées</div>
</div>
<div className="afl-arrivees__header-center">
{totalPages > 1 && Array.from({ length: totalPages }).map((_, i) => (
<div key={i} className={`afl-arrivees__page-dot ${i === pageIndex ? 'afl-arrivees__page-dot--active' : ''}`}>
<div className="afl-arr-header-center">
{totalPages > 1 && Array.from({ length: totalPages }).map((_, i) => (
<div key={i} className={`afl-arr-page-dot ${i === pageIndex ? 'afl-arr-page-dot--active' : ''}`}>
{i + 1}
</div>
))}
</div>
</div>
<div className="afl-arrivees__header-right">
<Clock />
</div>
</div>
<div className="afl-arr-header-right">
<Clock />
</div>
</header>
{/* LISTE DES TRAINS */}
<ul className="afl-arrivees__list">
{displayedHoraires.length === 0 ? (
<li className="afl-arrivees__row afl-arrivees__row--empty" key="empty">
<div style={{ color: 'white', padding: 24, fontSize: 24 }}>Aucun horaire disponible.</div>
</li>
) : displayedHoraires.map((h, idx) => {
const horaire = h || {};
const op = (horaire.type_train || horaire.op || '').toUpperCase();
const number = horaire.numero_train || horaire.number || horaire.numero || '';
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
{/* ROWS */}
<ul className="afl-arr-list">
{displayedHoraires.length === 0 ? (
<li className="afl-arr-row afl-arr-row--detailed" key="empty">
<div style={{ color: 'white', padding: 24, fontSize: 24 }}>Aucun horaire disponible.</div>
</li>
) : displayedHoraires.map((h, idx) => {
const horaire = h || {};
const isDetailed = isPage1 && idx < 4;
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || '';
const platform = (horaire._displayPlatform || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '').toString().trim();
const op = (horaire.type_train || horaire.op || horaire.type || '').toUpperCase();
const number = horaire.numero_train || horaire.number || horaire.numero || '';
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null);
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const rawPlatform = (
horaire._displayPlatform ||
horaire.quai_reel ||
horaire.quai ||
horaire.quai_prevu ||
horaire.platform ||
horaire.voie ||
horaire.track ||
''
).toString().trim();
// Removed time = '' to stop alternation
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
let showPlatform = !(status && status.label === 'supprimé') && Boolean(platform);
if (showPlatform) {
const threshold = thresholdForStationType(stationType);
let diffMinutes = null;
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_arrivee || horaire.time || horaire.heure || horaire._rawTime || '');
if (sched !== null) {
let baseDate = new Date();
if (horaire._date) {
const parts = horaire._date.split('-');
if (parts.length === 3) {
baseDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
}
}
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
if (!horaire._date) {
if (sched < 240 && nowHM > 1200) baseDate.setDate(baseDate.getDate() + 1);
else if (sched > 1200 && nowHM < 240) baseDate.setDate(baseDate.getDate() - 1);
}
diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
}
if (diffMinutes !== null) {
showPlatform = (diffMinutes <= threshold);
} else {
showPlatform = false;
}
}
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
// Calcul des vias
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
let isAfterStart = false;
for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
if (!sName) continue;
const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) {
isAfterStart = true;
continue;
}
if (isAfterStart) {
if (norm === normalizeName(destination || '')) {
break;
}
viaNames.push(sName);
}
}
}
const viasStr = viaNames.length > 0 ? viaNames.join(' > ') : '';
const marqueeStyle = viasStr.length > 0 ? { animationDuration: `${Math.max(6, viasStr.length * 0.25)}s` } : {};
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
const isDelayed = status.label && String(status.label).toLowerCase().includes('retard');
const isSuppressed = status.label && String(status.label).toLowerCase().includes('supprimé');
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null);
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
let displayStatusText = "À l'heure";
let delayVal = '';
if (isDelayed) {
displayStatusText = status.label;
if (typeof status.delayMinutes === 'number') {
delayVal = `+${status.delayMinutes} min`;
displayStatusText = delayVal;
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
let delayVal = '+ 5 min';
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
delayVal = `+ ${status.delayMinutes} min`;
} else if (status?.delayMinutes) {
delayVal = `+ ${status.delayMinutes} min`;
} else if (status?.label && status.label.includes('+')) {
delayVal = status.label;
}
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
// Calcul des vias (gares entre l'origine et la gare d'arrivée)
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
if (!sName) continue;
const norm = normalizeName(sName);
if (norm === normalizeName(origin || '')) continue;
if (norm === normalizeName(stationName || '')) break;
viaNames.push(sName);
}
}
if (isSuppressed) displayStatusText = "Supprimé";
const isBus = op.includes('CAR') || op.includes('AUTOCAR') || op.includes('BUS') || (horaire.mode && horaire.mode.toLowerCase().includes('bus'));
const renderPillIcon = () => {
if (isSuppressed) {
return (
<svg viewBox="0 0 20 20" className="afl-arr-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<path d="M6.5 6.5l7 7M13.5 6.5l-7 7" stroke="#ff5c73" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
}
if (isDelayed) {
return (
<svg viewBox="0 0 20 20" className="afl-arr-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<line x1="10" y1="4.5" x2="10" y2="11.5" stroke="white" strokeWidth="2.4" strokeLinecap="round" />
<circle cx="10" cy="14.8" r="1.4" fill="white" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" className="afl-arr-icon">
<circle cx="10" cy="10" r="9.5" fill="white" />
<path d="M6 10.2l2.6 2.6 5.4-5.4" stroke="#337862" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const isCompact = pageIndex > 0 && idx > 0;
const showCompactStatus = isCompact;
return (
<li key={idx} className={`afl-arr-row ${isDetailed ? 'afl-arr-row--detailed' : 'afl-arr-row--compact'}`}>
return (
<React.Fragment key={idx}>
<li className={`afl-arrivees__row ${isCompact ? 'afl-arrivees__row--compact' : ''}`}>
<div className="afl-arrivees__col-time-status">
{showCompactStatus && showAlternateStatus ? (
<div className={`afl-arrivees__status-pill ${isSuppressed ? 'afl-arrivees__status-pill--suppressed' : (isDelayed ? 'afl-arrivees__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#216b3e" />
</svg>
)}
<span>{displayStatusText}</span>
{/* TIME & PILL */}
<div className="afl-arr-col-time">
{isDetailed ? (
<>
<div className="afl-arr-top-row">
<div className="afl-arr-text-time">{time}</div>
</div>
) : (
<>
<div className="afl-arrivees__time">{time}</div>
{!isCompact && (
<div className={`afl-arrivees__status-pill ${isSuppressed ? 'afl-arrivees__status-pill--suppressed' : (isDelayed ? 'afl-arrivees__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#216b3e" />
</svg>
)}
<span>{displayStatusText}</span>
</div>
)}
</>
)}
</div>
<div className="afl-arrivees__col-train-info">
<div className="afl-arrivees__op-name">{op}</div>
{!isCompact && <div className="afl-arrivees__op-number">{number}</div>}
</div>
<div className="afl-arrivees__col-destination">
<div className="afl-arrivees__dest-line">
{isBus && (
<svg viewBox="0 0 24 24" width="28" height="28" fill="white">
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z"/>
</svg>
)}
<MarqueeText text={origin} className="afl-arrivees__destination-name" />
</div>
{!isCompact && (
<div className="afl-arrivees__vias-wrapper">
{viasStr ? (
<>
<span className="afl-arrivees__vias-static">via </span>
<div className="afl-arrivees__vias-container">
<div className="afl-arrivees__vias" style={marqueeStyle}>{viasStr}</div>
</div>
</>
) : (
<span className="afl-arrivees__vias-static">direct</span>
)}
</div>
)}
</div>
<div className="afl-arrivees__col-platform">
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="8vh" height="8vh">
<circle cx="12" cy="12" r="11" fill="#ff4c5b" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="white" />
</svg>
) : showPlatform ? (
<div className="afl-arrivees__platform-box">
{!isBus && <div className="afl-arrivees__platform-label">Voie</div>}
<div className="afl-arrivees__platform-value">
{isBus ? (
<svg viewBox="0 0 24 24" width="7vh" height="7vh" fill="#1e4a30">
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z"/>
</svg>
) : platform}
<div className="afl-arr-bottom-row">
<div className={`afl-arr-pill ${pillClass}`}>
{renderPillIcon()}
<span className="afl-arr-pill-text">{pillText}</span>
</div>
</div>
) : null}
</div>
</li>
{idx < displayedHoraires.length - 1 && (
<div className="afl-arrivees__separator-wrapper">
<div className="afl-arrivees__separator" />
</div>
)}
</React.Fragment>
);
})}
</ul>
</div>
</>
) : (
alternateDelay ? (
<div className={`afl-arr-pill ${pillClass}`}>
{renderPillIcon()}
<span className="afl-arr-pill-text">{pillText}</span>
</div>
) : (
<div className="afl-arr-text-time">{time}</div>
)
)}
</div>
{/* OP & NUMBER */}
<div className="afl-arr-col-train">
{isDetailed ? (
<>
<div className="afl-arr-top-row">
<div className="afl-arr-text-op">{op}</div>
</div>
<div className="afl-arr-bottom-row">
<div className="afl-arr-text-num">{number}</div>
</div>
</>
) : (
alternateDelay ? (
<div className="afl-arr-text-num">{op || number}</div>
) : (
<div className="afl-arr-text-num">{number || op}</div>
)
)}
</div>
{/* ORIGIN & VIAS */}
<div className="afl-arr-col-dest">
{isDetailed ? (
<>
<div className="afl-arr-top-row">
{isCarTER && <BusIcon size="5.5vh" color="white" style={{ marginRight: '0.8vw' }} />}
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
</div>
<div className="afl-arr-bottom-row">
<ViasMarquee viaNames={viaNames} />
</div>
</>
) : (
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
{isCarTER && <BusIcon size="5.0vh" color="white" style={{ marginRight: '0.8vw' }} />}
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
</div>
)}
</div>
{/* VOIE */}
<div className="afl-arr-col-voie">
{isDetailed ? (
isSuppressed ? (
<svg viewBox="0 0 24 24" width="8.5vh" height="8.5vh" style={{ flexShrink: 0 }}>
<circle cx="12" cy="12" r="11" fill="#ff5c73" />
<path d="M8 8l8 8M16 8l-8 8" stroke="#173e31" strokeWidth="3.2" strokeLinecap="round" />
</svg>
) : isCarTER ? (
<div className="afl-arr-voie-box afl-arr-voie-box--bus">
<BusIcon size="9.5vh" color="#173e31" />
</div>
) : showPlatform ? (
<div className="afl-arr-voie-box">
<div className="afl-arr-voie-label">Voie</div>
<div className="afl-arr-voie-value">{rawPlatform}</div>
</div>
) : null
) : (
isSuppressed ? (
<svg viewBox="0 0 24 24" width="6.0vh" height="6.0vh" style={{ flexShrink: 0 }}>
<circle cx="12" cy="12" r="11" fill="#ff5c73" />
<path d="M8 8l8 8M16 8l-8 8" stroke="#173e31" strokeWidth="3.2" strokeLinecap="round" />
</svg>
) : isCarTER ? (
<div className="afl-arr-voie-box-compact">
<BusIcon size="5.2vh" color="#173e31" />
</div>
) : null
)}
</div>
</li>
);
})}
</ul>
</div>
);
}
+225 -279
View File
@@ -1,334 +1,280 @@
/* Déclaration des fontes Achemine (dans public/fonts/) */
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
/* ==========================================================================
AFL ARRIVEES QUAIS (AFFICHAGE QUAI ARRIVÉE SNCF) - CSS
========================================================================== */
/* Conteneur principal (100vw x 100vh) */
.afl-q-arr-container {
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
background-color: #173e31 !important; /* Vert sombre SNCF Arrivées */
color: #ffffff !important;
width: 100vw !important;
height: 100vh !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
z-index: 9999 !important;
user-select: none !important;
}
.afl-quais-arrivees__container {
--bg-color: #1e4a30; /* Deep dark green background */
--header-bg: #216b3e; /* Lighter vibrant green header */
--accent-green: #4caf50; /* Green timeline/active color */
--accent-yellow: #fce205; /* Bright yellow for arrival time */
--text-grey: #a4d2b1; /* Light green-grey for train info */
.afl-q-arr-container * {
box-sizing: border-box !important;
}
.afl-quais-arrivees__container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
background-color: var(--bg-color);
color: white;
font-family: 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
overflow: hidden;
position: relative;
/* ==========================================================================
HEADER (10.5vh) : BANDEAU VERT MOYEN
========================================================================== */
.afl-q-arr-header {
height: 10.5vh !important;
min-height: 10.5vh !important;
max-height: 10.5vh !important;
background-color: #337862 !important; /* Vert moyen SNCF Arrivées */
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 2.5vw !important;
border-bottom: 2px solid rgba(255, 255, 255, 0.4) !important;
position: relative !important;
z-index: 10 !important;
}
/* --- HEADER --- */
.afl-quais-arrivees__header {
height: 12vh;
background-color: var(--header-bg);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 3vw;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
box-sizing: border-box;
.afl-q-arr-header-left {
display: flex !important;
align-items: center !important;
gap: 1.2vw !important;
}
.afl-quais-arrivees__header-left {
display: flex;
align-items: center;
gap: 1.2vw;
.afl-q-arr-header-icon {
flex-shrink: 0 !important;
}
.afl-quais-arrivees__header-title {
font-size: 5vh;
font-weight: 700;
letter-spacing: -0.02em;
.afl-q-arr-header-title {
font-size: 5.6vh !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: -0.03vw !important;
line-height: 1 !important;
}
.afl-quais-arrivees__header-clock {
font-size: 5vh;
font-weight: 700;
.afl-q-arr-header-clock {
font-size: 5.8vh !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: -0.04vw !important;
line-height: 1 !important;
}
/* --- MAIN CONTENT --- */
.afl-quais-arrivees__main {
flex: 1;
display: flex;
position: relative;
box-sizing: border-box;
/* ==========================================================================
CORPS PRINCIPAL (MAIN)
========================================================================== */
.afl-q-arr-main {
flex: 1 !important;
display: flex !important;
align-items: flex-start !important; /* Hauteur dynamique du panneau gauche */
padding: 2.5vh 2.5vw 1.5vh 2.5vw !important;
gap: 3.5vw !important;
overflow: hidden !important;
position: relative !important;
}
/* --- LEFT PANE (GLUED TO LEFT/TOP, ROUNDED BOTTOM RIGHT) --- */
.afl-quais-arrivees__left-pane {
position: absolute;
top: 0;
left: 0;
width: 35vw;
height: 38vh;
background: rgba(255, 255, 255, 0.07);
border-radius: 0 0 24px 0; /* Only bottom-right is rounded */
padding: 4vh 3vw;
display: flex;
flex-direction: column;
box-sizing: border-box;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-right: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
/* --------------------------------------------------------------------------
COLONNE GAUCHE (CARTE DU TRAIN - HAUTEUR DYNAMIQUE)
-------------------------------------------------------------------------- */
.afl-q-arr-left-card {
width: 29.5vw !important;
min-width: 29.5vw !important;
max-width: 29.5vw !important;
height: auto !important; /* Dynamique selon le nombre d'origines */
background-color: rgba(23, 62, 49, 0.90) !important;
border: 1.5px solid rgba(255, 255, 255, 0.14) !important;
border-radius: 1.4vh !important;
padding: 1.8vh 1.6vw !important;
display: flex !important;
flex-direction: column !important;
gap: 1.4vh !important;
box-shadow: 0 0.8vh 2.0vh rgba(0, 0, 0, 0.35) !important;
position: relative !important;
}
.afl-quais-arrivees__time-status {
display: flex;
align-items: center;
gap: 1vw;
margin-bottom: 1.5vh;
/* En-tête de carte : Heure & Badge Statut */
.afl-q-arr-card-top {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding-bottom: 1.4vh !important;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.14) !important;
}
.afl-quais-arrivees__time {
font-size: 7.5vh;
font-weight: 800;
color: var(--accent-yellow);
line-height: 1;
.afl-q-arr-card-time {
font-size: 5.6vh !important;
font-weight: 800 !important;
color: #ffff00 !important; /* Même couleur jaune que sur AFLArrivees.js */
line-height: 1 !important;
letter-spacing: -0.04vw !important;
}
.afl-quais-arrivees__status-pill {
display: inline-flex;
align-items: center;
background-color: #216b3e; /* Green background matching header */
border-radius: 3vh;
padding: 0.6vh 0.8vw;
font-size: 2.2vh;
font-weight: 600;
color: white;
width: max-content;
/* Pastille Statut */
.afl-q-arr-pill {
display: inline-flex !important;
align-items: center !important;
gap: 0.6vw !important;
padding: 0.5vh 1.0vw 0.5vh 0.7vw !important;
border-radius: 2.5vh !important;
background-color: #337862 !important;
font-size: 2.4vh !important;
font-weight: 700 !important;
color: #ffffff !important;
line-height: 1 !important;
}
.afl-quais-arrivees__status-pill svg {
margin-right: 0.5vw;
.afl-q-arr-pill--delayed {
background-color: #f8c898 !important;
color: #111111 !important;
}
.afl-quais-arrivees__status-pill--delayed {
background-color: #ff9e43;
color: #111;
.afl-q-arr-pill--suppressed {
background-color: #ff5c73 !important;
color: #111111 !important;
}
.afl-quais-arrivees__status-pill--suppressed {
background-color: #ff4c5b;
color: #111;
.afl-q-arr-pill-icon {
width: 2.6vh !important;
height: 2.6vh !important;
flex-shrink: 0 !important;
}
.afl-quais-arrivees__destination {
font-size: 5.5vh;
font-weight: 700;
line-height: 1.15;
color: #ffffff;
margin-bottom: 1.5vh;
word-wrap: break-word;
/* Provenances & Numéros */
.afl-q-arr-dest-block {
padding-bottom: 1.4vh !important;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.14) !important;
display: flex !important;
flex-direction: column !important;
gap: 0.6vh !important;
}
.afl-quais-arrivees__train-info {
font-size: 3vh;
color: var(--text-grey);
display: flex;
gap: 0.8vw;
.afl-q-arr-dest-title {
font-size: 5.6vh !important; /* Grande police pour la gare de provenance */
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1.05 !important;
letter-spacing: -0.03vw !important;
word-break: break-word !important;
}
.afl-quais-arrivees__train-type {
font-weight: 800;
.afl-q-arr-dest-sub {
font-size: 2.6vh !important;
font-weight: 700 !important;
color: #9be0c8 !important;
letter-spacing: 0.04vw !important;
text-transform: uppercase !important;
line-height: 1 !important;
margin-top: 0.4vh !important;
}
.afl-quais-arrivees__train-number {
font-weight: 500;
/* Bas de la carte : Logo du type de train */
.afl-q-arr-card-footer {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
padding-top: 0.4vh !important;
}
/* --- RIGHT PANE (TIMELINE) --- */
.afl-quais-arrivees__right-pane {
position: absolute;
top: 4vh;
left: 38vw;
width: 58vw;
bottom: 12vh;
display: flex;
flex-direction: column;
justify-content: center; /* Center stops list vertically */
box-sizing: border-box;
.afl-q-arr-train-logo-box {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
max-height: 4.8vh !important;
}
.afl-quais-arrivees__stops-window {
width: 100%;
max-height: 100%;
overflow: hidden;
position: relative;
.afl-q-arr-train-logo-img {
max-height: 4.5vh !important;
max-width: 15vw !important;
object-fit: contain !important;
}
.afl-quais-arrivees__stops-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
.afl-q-arr-train-logo-text {
border: 1.5px solid rgba(255, 255, 255, 0.85) !important;
border-radius: 0.6vh !important;
padding: 0.3vh 0.8vw !important;
font-size: 2.5vh !important;
font-style: italic !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: 0.06vw !important;
}
.afl-quais-arrivees__stop-item {
display: flex;
align-items: center;
height: 6.2vh;
position: relative;
box-sizing: border-box;
/* --------------------------------------------------------------------------
COLONNE DROITE (TIMELINE / PLAN DE LIGNE DYNAMIQUE ARRIVEE)
-------------------------------------------------------------------------- */
.afl-q-arr-timeline-area {
flex: 1 !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
overflow: hidden !important;
position: relative !important;
}
/* Timeline Indicator (Line segment + Dot) */
.afl-quais-arrivees__stop-indicator {
position: relative;
width: 4vw;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.afl-q-arr-timeline-svg {
width: 100% !important;
height: 100% !important;
overflow: visible !important;
}
.afl-quais-arrivees__stop-line {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 0.6vh;
transform: translateX(-50%);
background-color: var(--accent-green);
box-shadow: 0 0 6px rgba(76, 175, 80, 0.4);
z-index: 1;
/* ==========================================================================
FOOTER : BOX VOIE EN BAS À DROITE (SANS REPÈRES DE QUAI NI COMPOSITION)
========================================================================== */
.afl-q-arr-footer {
height: 14vh !important;
min-height: 14vh !important;
max-height: 14vh !important;
display: flex !important;
align-items: flex-end !important;
justify-content: flex-end !important;
padding: 0 2.5vw 1.8vh 2.5vw !important;
position: relative !important;
z-index: 10 !important;
}
/* Adjust line ends for first and last stops */
.afl-quais-arrivees__stop-item.is-first .afl-quais-arrivees__stop-line {
top: 50%;
/* --------------------------------------------------------------------------
BOX VOIE (BLANCHE / PROPORTIONS STRICTES)
-------------------------------------------------------------------------- */
.afl-q-arr-voie-box {
background-color: #ffffff !important;
border-radius: 1.2vh !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
width: 7.2vw !important;
min-width: 7.2vw !important;
height: 12.5vh !important;
padding: 0.4vh 0 !important;
box-shadow: 0 0.6vh 1.5vh rgba(0, 0, 0, 0.35) !important;
flex-shrink: 0 !important;
}
.afl-quais-arrivees__stop-item.is-last .afl-quais-arrivees__stop-line {
bottom: 50%;
.afl-q-arr-voie-label {
color: #173e31 !important;
font-size: 2.4vh !important;
font-weight: 600 !important;
line-height: 1 !important;
letter-spacing: -0.02vw !important;
margin: 0 !important;
}
/* Standard dot */
.afl-quais-arrivees__stop-dot {
width: 1.4vh;
height: 1.4vh;
border-radius: 50%;
background-color: var(--accent-green);
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
z-index: 2;
position: relative;
}
/* Terminus dot */
.afl-quais-arrivees__stop-dot.is-terminus {
width: 2.6vh;
height: 2.6vh;
background-color: var(--bg-color); /* Hollow effect */
border: 0.6vh solid var(--accent-green);
box-shadow: 0 0 8px rgba(76, 175, 80, 0.8);
border-radius: 50%;
}
/* Stop Names */
.afl-quais-arrivees__stop-name {
font-size: 3.8vh;
font-weight: 500;
color: #ffffff;
padding-left: 1vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.afl-quais-arrivees__stop-name.is-terminus {
font-size: 4.5vh;
font-weight: 700;
color: #ffffff;
padding-left: 1vw;
}
/* --- BOTTOM TRAIN ILLUSTRATION --- */
.afl-quais-arrivees__bottom-container {
position: absolute;
bottom: 3.5vh;
left: 38vw;
width: 42vw;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.afl-quais-arrivees__train-illustration {
display: flex;
justify-content: center;
opacity: 0.85;
}
/* --- VOIE BOX (PLATFORM BOX) --- */
.afl-quais-arrivees__platform-box {
position: absolute;
bottom: 2vh;
right: 2vw;
background-color: #ffffff;
border-radius: 8px;
width: 6.5vw;
height: 13vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.35);
z-index: 10;
box-sizing: border-box;
}
.afl-quais-arrivees__platform-label {
font-size: 2.2vh;
color: var(--bg-color);
font-weight: 700;
text-transform: capitalize;
line-height: 1;
margin-bottom: 0.5vh;
}
.afl-quais-arrivees__platform-value {
font-size: 7vh;
color: var(--bg-color);
font-weight: 800;
line-height: 1;
}
/* --- NO DEPARTURE / LOADING STATE --- */
.afl-quais-arrivees__no-departure {
display: flex;
justify-content: center;
align-items: center;
height: 88vh;
font-size: 6vh;
font-weight: 600;
color: #a4d2b1;
.afl-q-arr-voie-value {
color: #173e31 !important;
font-size: 6.8vh !important;
font-weight: 800 !important;
line-height: 0.9 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
+277 -244
View File
@@ -3,6 +3,7 @@ import supabase from '../../../lib/supabase';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLArriveesQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
@@ -25,56 +26,18 @@ const AFLArriveesQuais = () => {
const [nextArrival, setNextArrival] = useState(null);
const [loading, setLoading] = useState(true);
// Parse URL manually since we are not in a React Router context
const searchParams = new URLSearchParams(window.location.search);
const gareParam = searchParams.get('gare') || '';
const gareIdParam = searchParams.get('gare_id') || searchParams.get('gareId');
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
useAnnonceSonoreRealtime(resolvedGareId, 'AFLArriveesQuais', quaiParam);
useAnnonceSonoreRealtime(null, 'AFLArriveesQuais', quaiParam);
useEffect(() => {
let mounted = true;
const resolveGareId = async () => {
if (!supabase) return;
try {
if (gareIdParam) {
const { data: gdata } = await supabase.from('gares').select('id').eq('id', gareIdParam).limit(1);
if (mounted && gdata && gdata.length) {
setResolvedGareId(gdata[0].id);
}
} else if (gareDisplay) {
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata } = await supabase.from('gares').select('id, nom').ilike('nom', flexNeedle);
if (gdata && gdata.length) {
const exactMatch = gdata.find(g => {
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
if (mounted && chosen) {
setResolvedGareId(chosen.id);
}
}
}
} catch (e) {
console.error('[AFLArriveesQuais] Erreur resolution gare:', e);
}
};
resolveGareId();
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
// Real-time clock update
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
// --- Helpers pour perturbations, travaux, etc ---
const normalizeName = useCallback((s) => {
if (!s) return '';
try {
@@ -85,20 +48,6 @@ const AFLArriveesQuais = () => {
}
}, []);
const safeParseArrets = useCallback((p) => {
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
if (!raw) return [];
if (Array.isArray(raw)) return raw;
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
if (Array.isArray(parsed.arrets)) return parsed.arrets;
return parsed;
}
return [];
} catch (e) { return []; }
}, []);
const getStops = useCallback((h) => {
if (!h) return [];
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
@@ -116,6 +65,20 @@ const AFLArriveesQuais = () => {
return [];
}, []);
const safeParseArrets = useCallback((p) => {
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
if (!raw) return [];
if (Array.isArray(raw)) return raw;
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
if (Array.isArray(parsed.arrets)) return parsed.arrets;
return parsed;
}
return [];
} catch (e) { return []; }
}, []);
const parsePerturbArretsObject = useCallback((p) => {
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
if (!raw) return { arrets: [], jours: null };
@@ -157,18 +120,8 @@ const AFLArriveesQuais = () => {
else pushDate(candidate.date || null);
}
}
try {
const parsedObj = parsePerturbArretsObject(p);
const jp = parsedObj.jours;
if (jp) {
if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it));
else if (typeof jp === 'string') {
try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); }
} else if (typeof jp === 'object') pushDate(jp.date || null);
}
} catch (e) {}
return result.size ? result : null;
}, [parsePerturbArretsObject]);
}, []);
const parseDelayMinutesFromArret = useCallback((arret) => {
if (!arret) return null;
@@ -192,8 +145,6 @@ const AFLArriveesQuais = () => {
return null;
}, []);
const buildPerturbMap = useCallback((list, allHorairesList) => {
const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] };
(list || []).forEach(p => {
@@ -212,28 +163,9 @@ const AFLArriveesQuais = () => {
}
if (horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length)) { idx.global.push(p); }
});
if (Array.isArray(allHorairesList) && allHorairesList.length) {
try {
const byH = Object.keys(idx.byHoraire || {});
if (byH && byH.length) {
for (const hid of byH) {
const h = allHorairesList.find(x => String(x.id) === String(hid));
if (h) {
const num = h.numero_train || h.number || h.numero || null;
if (num != null) {
const numKey = String(num);
idx.byNumber[numKey] = idx.byNumber[numKey] || [];
(idx.byHoraire[hid] || []).forEach(p => idx.byNumber[numKey].push(p));
}
}
}
}
} catch (e) {}
}
return idx;
}, [safeParseArrets, normalizeName]);
const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
const map = idx || buildPerturbMap();
const allPerturbs = [];
@@ -691,192 +623,293 @@ const AFLArriveesQuais = () => {
return () => clearInterval(interval);
}, [fetchData]);
if (loading) return <div className="afl-quais-arrivees__container" style={{background: '#1e4a30'}}></div>;
if (!nextArrival) return (
<div className="afl-quais-arrivees__container">
<div className="afl-quais-arrivees__header">
<div className="afl-quais-arrivees__header-left">
<svg viewBox="0 0 24 24" width="6.5vh" height="6.5vh" fill="white" style={{ marginRight: '0.5vw' }}>
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
</svg>
<div className="afl-quais-arrivees__header-title">Prochaine arrivée</div>
</div>
<div className="afl-quais-arrivees__header-clock">{formatTimeNoSeconds(currentTime)}</div>
if (loading) return <div className="afl-q-arr-container" style={{ background: '#173e31' }}></div>;
if (!nextArrival) {
return (
<div className="afl-q-arr-container">
<header className="afl-q-arr-header">
<div className="afl-q-arr-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-arr-header-icon">
<path d="M6 6v20M6 6h3M6 26h3" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="12" cy="10" r="2.6" fill="white" />
<path d="M10 14h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M22 8l5 5-5 5M16 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="afl-q-arr-header-title">Prochaine arrivée</div>
</div>
<div className="afl-q-arr-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '4.0vh', color: '#ffffff', fontWeight: 600 }}>
Aucune arrivée prévue
</div>
<div className="afl-quais-arrivees__no-departure">Aucune arrivée prévue</div>
</div>
);
const stops = getStops(nextArrival);
let filteredStops = [];
if (stops.length > 0 && gareDisplay) {
const stationIndex = stops.findIndex(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
if (stationIndex !== -1) {
filteredStops = stops.slice(0, stationIndex);
} else {
filteredStops = stops.slice();
}
} else {
filteredStops = stops.slice();
<footer className="afl-q-arr-footer">
{quaiParam && (
<div className="afl-q-arr-voie-box">
<div className="afl-q-arr-voie-label">Voie</div>
<div className="afl-q-arr-voie-value">{quaiParam}</div>
</div>
)}
</footer>
</div>
);
}
const originName = nextArrival.gare_depart_nom || nextArrival.gare_depart || 'Origine';
if (filteredStops.length > 0) {
const firstStopName = normalizeName((filteredStops[0].gare_nom || filteredStops[0].name || filteredStops[0].nom || '').toString());
const normOrigin = normalizeName(originName);
if (firstStopName !== normOrigin && !firstStopName.includes(normOrigin) && !normOrigin.includes(firstStopName)) {
filteredStops.unshift({ gare_nom: originName, isFirst: true });
} else {
filteredStops[0].isFirst = true;
}
} else {
filteredStops.unshift({ gare_nom: originName, isFirst: true });
const trainData = nextArrival;
const isSuppressed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('supprimé');
const isDelayed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('retard');
let delayVal = '+ 5 min';
if (typeof trainData._status?.delayMinutes === 'number' && trainData._status.delayMinutes > 0) {
delayVal = `+ ${trainData._status.delayMinutes} min`;
} else if (trainData._status?.label && trainData._status.label.includes('+')) {
delayVal = trainData._status.label;
}
const targetStationName = gareDisplay || 'Terminus';
filteredStops.push({ gare_nom: targetStationName, isLast: true });
const pillClass = isSuppressed ? 'afl-q-arr-pill--suppressed' : isDelayed ? 'afl-q-arr-pill--delayed' : '';
const pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayVal : "À l'heure";
// Deduplicate stops by name
const seenStops = new Set();
filteredStops = filteredStops.filter(s => {
const name = normalizeName((s.gare_nom || s.name || s.nom || s.stop_name || '').toString());
if (!name) return false;
if (seenStops.has(name)) return false;
seenStops.add(name);
return true;
});
const isSuppressed = nextArrival._status && nextArrival._status.label === 'supprimé';
const isDelayed = nextArrival._status && nextArrival._status.label && String(nextArrival._status.label).toLowerCase().includes('retard');
let displayStatusText = "À l'heure";
if (isSuppressed) displayStatusText = "Supprimé";
else if (isDelayed) {
if (typeof nextArrival._status.delayMinutes === 'number') {
displayStatusText = `+${nextArrival._status.delayMinutes} min`;
} else {
displayStatusText = nextArrival._status.label;
const renderPillIcon = () => {
if (isSuppressed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-arr-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<path d="M6.5 6.5l7 7M13.5 6.5l-7 7" stroke="#ff5c73" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
}
}
if (isDelayed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-arr-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<line x1="10" y1="4.5" x2="10" y2="11.5" stroke="white" strokeWidth="2.4" strokeLinecap="round" />
<circle cx="10" cy="14.8" r="1.4" fill="white" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" className="afl-q-arr-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="white" />
<path d="M6 10.2l2.6 2.6 5.4-5.4" stroke="#337862" strokeWidth="2.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const displayTime = nextArrival._displayTime || formatTimeNoSeconds(nextArrival.heure_arrivee || nextArrival.time || nextArrival.heure || '');
const trainType = nextArrival.type_train || nextArrival.op || 'TER';
const trainNum = nextArrival.numero_train || nextArrival.numero || nextArrival.number || '';
const displayTime = trainData._displayTime || formatTimeNoSeconds(trainData.heure_arrivee || trainData.time || trainData.heure || '');
const trainType = trainData.type_train || trainData.op || 'TER';
const trainNum = trainData.numero_train || trainData.numero || trainData.number || '';
const originName = trainData.gare_depart_nom || trainData.gare_depart || 'Origine';
const stopsList = getStops(nextArrival);
const stopsList = getStops(trainData);
let stopForStation = null;
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || nextArrival.quai_reel || nextArrival.voie || nextArrival.quai || quaiParam || '').toString().trim();
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim();
const shouldScroll = filteredStops.length > 10;
// eslint-disable-next-line no-unused-vars
const scrollStyle = shouldScroll ? {
animation: `scrollStops ${filteredStops.length * 2.2}s linear infinite`
} : {};
// Extraction des origines (si multi-origines / tranches)
let origins = [];
if (Array.isArray(trainData.tranches) && trainData.tranches.length > 0) {
origins = trainData.tranches.map(t => ({
name: t.gare_depart_nom || t.origine || t.nom,
op: t.type_train || t.op || trainType,
number: t.numero_train || t.number || trainNum
}));
} else if (originName.includes('/')) {
origins = originName.split('/').map(d => ({
name: d.trim(),
op: trainType,
number: trainNum
}));
} else {
origins = [{
name: originName,
op: trainType,
number: trainNum
}];
}
// Formatage des lignes d'origines (découpage élégant sur 2 lignes si nécessaire)
const formatDestLines = (name) => {
if (!name) return ['Origine'];
const clean = name.trim();
if (clean.includes('Marseille Saint-Charles')) return ['Marseille Saint-', 'Charles'];
if (clean.includes('Montpellier Saint-Roch')) return ['Montpellier', 'Saint-Roch'];
if (clean.includes('Paris Gare de Lyon')) return ['Paris', 'Gare de Lyon'];
if (clean.includes('Paris Montparnasse')) return ['Paris', 'Montparnasse'];
if (clean.includes('Bourg-en-Bresse')) return ['Bourg-en-', 'Bresse'];
if (clean.length > 12) {
const parts = clean.split(' ');
if (parts.length >= 2) {
const mid = Math.ceil(parts.length / 2);
return [parts.slice(0, mid).join(' '), parts.slice(mid).join(' ')];
}
}
return [clean];
};
// Calcul dynamique des arrêts desservis en amont (depuis l'origine jusqu'à la gare actuelle)
const currentStationName = gareDisplay || (stopForStation && (stopForStation.gare_nom || stopForStation.nom || stopForStation.name)) || (stopsList[stopsList.length - 1] && (stopsList[stopsList.length - 1].gare_nom || stopsList[stopsList.length - 1].nom || stopsList[stopsList.length - 1].name)) || 'Terminus';
let journeyStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) {
const normCurrent = normalizeName(currentStationName);
const endIdx = stopsList.findIndex(s => {
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
});
const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice();
const seen = new Set();
rawStops.forEach(s => {
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
const norm = normalizeName(name);
if (name && !seen.has(norm)) {
seen.add(norm);
journeyStops.push(name);
}
});
}
// Si aucun arrêt n'est trouvé, afficher l'origine et le terminus actuel
if (journeyStops.length === 0) {
journeyStops = [originName, currentStationName];
} else if (journeyStops.length === 1 && journeyStops[0] !== currentStationName) {
journeyStops.push(currentStationName);
}
const startStopName = journeyStops[0];
const subsequentStops = journeyStops.slice(1);
return (
<div className="afl-quais-arrivees__container">
{/* Header */}
<div className="afl-quais-arrivees__header">
<div className="afl-quais-arrivees__header-left">
<svg viewBox="0 0 24 24" width="6.5vh" height="6.5vh" fill="white" style={{ marginRight: '0.5vw' }}>
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
</svg>
<div className="afl-quais-arrivees__header-title">Prochaine arrivée</div>
<div className="afl-q-arr-container">
{/* HEADER (10.5vh) */}
<header className="afl-q-arr-header">
<div className="afl-q-arr-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-arr-header-icon">
<path d="M6 6v20M6 6h3M6 26h3" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="12" cy="10" r="2.6" fill="white" />
<path d="M10 14h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M22 8l5 5-5 5M16 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="afl-q-arr-header-title">Prochaine arrivée</div>
</div>
<div className="afl-quais-arrivees__header-clock">{formatTimeNoSeconds(currentTime)}</div>
</div>
<div className="afl-q-arr-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div className="afl-quais-arrivees__main">
{/* Left Pane (Floating Card) */}
<div className="afl-quais-arrivees__left-pane">
<div className="afl-quais-arrivees__time-status">
<div className="afl-quais-arrivees__time">{displayTime}</div>
<div className={`afl-quais-arrivees__status-pill ${isSuppressed ? 'afl-quais-arrivees__status-pill--suppressed' : (isDelayed ? 'afl-quais-arrivees__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#216b3e" />
</svg>
)}
<span>{displayStatusText}</span>
{/* CORPS PRINCIPAL */}
<main className="afl-q-arr-main">
{/* PANNEAU GAUCHE : CARTE D'INFORMATION DU TRAIN */}
<section className="afl-q-arr-left-card">
{/* Ligne Heure d'arrivée & Statut */}
<div className="afl-q-arr-card-top">
<div className="afl-q-arr-card-time">{displayTime}</div>
<div className={`afl-q-arr-pill ${pillClass}`}>
{renderPillIcon()}
<span>{pillText}</span>
</div>
</div>
<div className="afl-quais-arrivees__destination">{originName}</div>
<div className="afl-quais-arrivees__train-info">
<span className="afl-quais-arrivees__train-type">{trainType}</span>
<span className="afl-quais-arrivees__train-number">{trainNum}</span>
</div>
</div>
{/* Right Pane (Timeline) */}
<div className="afl-quais-arrivees__right-pane">
<div className="afl-quais-arrivees__stops-window">
<style>
{`
@keyframes scrollStops {
0%, 15% { transform: translateY(0); }
85%, 100% { transform: translateY(calc(-100% + 65vh)); }
}
`}
</style>
<ul className="afl-quais-arrivees__stops-list" style={shouldScroll ? { animation: `scrollStops ${filteredStops.length * 1.5}s ease-in-out infinite alternate` } : {}}>
{/* Main List */}
{filteredStops.map((s, i) => {
const sname = s.gare_nom || s.nom || s.name || s.stop_name || '';
const isFirst = i === 0;
const isLastItem = s.isLast || i === filteredStops.length - 1;
{/* Blocs Origines dynamiques selon l'horaire */}
{origins.map((d, i) => {
const lines = formatDestLines(d.name);
return (
<div key={i} className="afl-q-arr-dest-block">
{lines.map((line, lIdx) => (
<div key={lIdx} className="afl-q-arr-dest-title">{line}</div>
))}
<div className="afl-q-arr-dest-sub">
{d.op} {d.number}
</div>
</div>
);
})}
{/* Pied de carte : Logo du type de train */}
<div className="afl-q-arr-card-footer">
<Logo
op={trainType}
classNameBox="afl-q-arr-train-logo-box"
classNameImg="afl-q-arr-train-logo-img"
classNameText="afl-q-arr-train-logo-text"
/>
</div>
</section>
{/* PANNEAU DROIT : TIMELINE ARBORESCENTE DYNAMIQUE DE L'ARRIVEE */}
<section className="afl-q-arr-timeline-area">
<svg viewBox="0 0 850 560" className="afl-q-arr-timeline-svg">
{/* 3 mini-points vert émeraude au sommet */}
<circle cx="45" cy="22" r="4" fill="#00e5a3" />
<circle cx="45" cy="38" r="4" fill="#00e5a3" />
<circle cx="45" cy="54" r="4" fill="#00e5a3" />
{/* Station 1: Gare d'origine */}
<circle cx="45" cy="92" r="17" fill="#173e31" stroke="#00e5a3" strokeWidth="4" />
<path d="M 37 89 L 45 97 L 53 89" stroke="#00e5a3" strokeWidth="3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<text x="82" y="102" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{startStopName}
</text>
{/* Tracé dynamique des arrêts vers le terminus actuel */}
{subsequentStops.length <= 1 ? (
<g>
<line x1="45" y1="109" x2="45" y2="480" stroke="#00e5a3" strokeWidth="5.5" strokeLinecap="round" />
<circle cx="45" cy="480" r="16" fill="#173e31" stroke="#00e5a3" strokeWidth="5" />
<text x="82" y="490" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{subsequentStops[0] || currentStationName}
</text>
</g>
) : (
(() => {
const N = subsequentStops.length;
const yStart = 160;
const yEnd = 500;
const step = Math.min(65, (yEnd - yStart) / Math.max(1, N - 1));
const lineEndY = yStart + (N - 1) * step;
return (
<li key={`s1-${i}`} className={`afl-quais-arrivees__stop-item ${isFirst ? 'is-first' : ''} ${isLastItem ? 'is-last' : ''}`}>
<div className="afl-quais-arrivees__stop-indicator">
<div className="afl-quais-arrivees__stop-line"></div>
<div className={`afl-quais-arrivees__stop-dot ${isLastItem ? 'is-terminus' : ''}`}></div>
</div>
<div className={`afl-quais-arrivees__stop-name ${isLastItem ? 'is-terminus' : ''}`}>{sname}</div>
</li>
<g>
<line x1="45" y1="109" x2="45" y2={lineEndY} stroke="#00e5a3" strokeWidth="5.5" strokeLinecap="round" />
{subsequentStops.map((stopName, sIdx) => {
const isLast = sIdx === N - 1;
const posY = yStart + sIdx * step;
if (!isLast) {
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="6.5" fill="#00e5a3" />
<text x="82" y={posY + 8} fill="#ffffff" fontSize="28" fontWeight="500" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
}
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="16" fill="#173e31" stroke="#00e5a3" strokeWidth="5" />
<text x="82" y={posY + 10} fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
})}
</g>
);
})}
</ul>
</div>
</div>
</div>
{/* Symmetrical train outline under the stops list */}
<div className="afl-quais-arrivees__bottom-container">
<div className="afl-quais-arrivees__train-illustration">
<svg viewBox="0 0 260 40" width="22vw" height="3.5vh">
<path fill="none" stroke="#4caf50" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
d="M30,35 L230,35 C242,35 248,31 254,22 L258,12 C260,8 256,4 247,4 L13,4 C4,4 0,8 2,12 L6,22 C12,31 18,35 30,35 Z" />
<line x1="55" y1="4" x2="55" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="82" y1="4" x2="82" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="109" y1="4" x2="109" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="136" y1="4" x2="136" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="163" y1="4" x2="163" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="190" y1="4" x2="190" y2="35" stroke="#4caf50" strokeWidth="2" />
<line x1="217" y1="4" x2="217" y2="35" stroke="#4caf50" strokeWidth="2" />
})()
)}
</svg>
</div>
</div>
</section>
</main>
{platformStr && (
<div className="afl-quais-arrivees__platform-box">
<div className="afl-quais-arrivees__platform-label">Voie</div>
<div className="afl-quais-arrivees__platform-value">{platformStr}</div>
{/* FOOTER : BOX VOIE EN BAS À DROITE */}
<footer className="afl-q-arr-footer">
<div className="afl-q-arr-voie-box">
<div className="afl-q-arr-voie-label">Voie</div>
<div className="afl-q-arr-voie-value">{platformStr}</div>
</div>
)}
</footer>
</div>
);
};
+392 -285
View File
@@ -1,382 +1,489 @@
/* Déclaration des fontes Achemine (dans public/fonts/) */
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
/* AFL DEPARTS - DESIGN STRICT CONFORME AUX MAQUETTES */
.afl-container {
font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important;
background-color: #002d62 !important;
color: #ffffff !important;
width: 100vw !important;
height: 100vh !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
z-index: 9999 !important;
user-select: none !important;
}
.afl-departs__container {
font-family: 'Achemine', Arial, 'Helvetica Neue', Helvetica, sans-serif;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
background-color: #012a6a; /* Vivid dark blue background */
overflow: hidden;
color: white;
margin: 0;
padding: 0;
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
z-index: 10000;
.afl-container * {
box-sizing: border-box !important;
}
.afl-departs__header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #005eb8; /* Bright blue header */
padding: 0 2vw;
height: 10vh;
flex-shrink: 0;
/* ==========================================================================
HEADER (10vh)
========================================================================== */
.afl-header {
height: 10vh !important;
min-height: 10vh !important;
max-height: 10vh !important;
background-color: #005fa9 !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 2.5vw !important;
border-bottom: 2px solid #ffffff !important;
}
.afl-departs__header-left {
display: flex;
align-items: center;
gap: 1.5vw;
width: 30vw;
.afl-header-left {
display: flex !important;
align-items: center !important;
gap: 1.2vw !important;
}
.afl-departs__title {
font-size: 5vh;
font-weight: 700;
.afl-header-title {
font-size: 5.5vh !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: 0.05vw !important;
line-height: 1 !important;
}
.afl-departs__header-center {
display: flex;
align-items: center;
justify-content: center;
gap: 1vw;
flex: 1;
.afl-header-center {
display: flex !important;
align-items: center !important;
gap: 0.8vw !important;
}
.afl-departs__page-dot {
width: 4vh;
height: 4vh;
border-radius: 50%;
background-color: #012a6a; /* Dark blue for inactive */
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2vh;
font-weight: 700;
.afl-page-dot {
width: 3.5vh !important;
height: 3.5vh !important;
border-radius: 50% !important;
background: rgba(255, 255, 255, 0.3) !important;
color: #ffffff !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 2.0vh !important;
font-weight: 700 !important;
}
.afl-departs__page-dot--active {
background-color: #00c3ff; /* Cyan for active */
.afl-page-dot--active {
background: #ffffff !important;
color: #005fa9 !important;
}
.afl-departs__header-right {
display: flex;
justify-content: flex-end;
width: 30vw;
.afl-header-right {
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
}
.afl-departs__header-right .aff-classiques-departs__clock-box {
background: transparent;
border: none;
box-shadow: none;
/* CLOCK */
.afl-clock-container {
display: flex !important;
align-items: baseline !important;
color: #ffffff !important;
}
.afl-departs__header-right .aff-classiques-departs__clock-main {
font-size: 4.5vh;
font-weight: 600;
color: #ffffff;
}
.afl-departs__header-right .aff-classiques-departs__clock-seconds {
display: none;
.afl-clock-main {
font-size: 6.0vh !important;
font-weight: 800 !important;
line-height: 1 !important;
letter-spacing: -0.05vw !important;
}
.afl-departs__list {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
.afl-clock-seconds {
font-size: 3.5vh !important;
font-weight: 700 !important;
margin-left: 0.3vw !important;
opacity: 0.85 !important;
line-height: 1 !important;
}
.afl-departs__row {
display: flex;
align-items: flex-start;
padding: 2.5vh 2vw 0 2vw;
position: relative;
height: 17.5vh;
box-sizing: border-box;
/* ==========================================================================
TRAIN ROWS LIST
========================================================================== */
.afl-list {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
background-color: #002d62 !important;
}
.afl-departs__col-time-status {
width: 16vw;
flex-shrink: 0;
display: flex;
flex-direction: column;
.afl-row {
display: flex !important;
width: 100vw !important;
border-bottom: 2px solid #ffffff !important;
padding-left: 2.5vw !important;
padding-right: 1.5vw !important;
background-color: #002d62 !important;
}
.afl-departs__time {
font-size: 7vh;
font-weight: 700;
color: #fce205; /* Bright yellow */
line-height: 1;
letter-spacing: -2px;
/* --------------------------------------------------------------------------
MODE ETENDU (20vh)
-------------------------------------------------------------------------- */
.afl-row--detailed {
height: 20vh !important;
min-height: 20vh !important;
max-height: 20vh !important;
align-items: stretch !important;
}
.afl-departs__time--suppressed {
color: #ff4444;
font-size: 4vh;
text-transform: uppercase;
.afl-row--detailed .afl-col-time,
.afl-row--detailed .afl-col-train,
.afl-row--detailed .afl-col-dest {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
height: 100% !important;
}
.afl-departs__status-pill {
display: inline-flex;
align-items: center;
background-color: #005eb8; /* Blue background matching header */
border-radius: 3vh;
padding: 0.6vh 0.8vw;
margin-top: 1vh;
font-size: 2.5vh;
font-weight: 500;
color: white;
width: max-content;
/* --------------------------------------------------------------------------
MODE REDUIT (10vh) - CENTRAGE PARFAIT DE TOUS LES TEXTES
-------------------------------------------------------------------------- */
.afl-row--compact {
height: 10vh !important;
min-height: 10vh !important;
max-height: 10vh !important;
align-items: center !important;
}
.afl-departs__status-pill svg {
margin-right: 0.5vw;
.afl-row--compact .afl-col-time,
.afl-row--compact .afl-col-train,
.afl-row--compact .afl-col-dest,
.afl-row--compact .afl-col-voie {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
height: 100% !important;
}
.afl-departs__status-pill--delayed {
background-color: #ff9e43;
color: #111;
/* Colonne 1 : Heure & Badge Statut (Centrage synchronisé sur 13.5vw) */
.afl-col-time {
width: 13.5vw !important;
min-width: 13.5vw !important;
max-width: 13.5vw !important;
margin-right: 2.5vw !important;
padding: 0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.afl-departs__status-pill--suppressed {
background-color: #ff4c5b;
color: #111;
.afl-row--detailed .afl-col-time .afl-top-row {
display: flex !important;
justify-content: center !important;
align-items: flex-end !important;
width: 100% !important;
}
.afl-departs__col-train-info {
width: 12vw;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding-top: 0.5vh;
align-items: flex-start; /* Left align type and number */
.afl-row--detailed .afl-col-time .afl-bottom-row {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
}
.afl-departs__op-name {
font-size: 3vh;
font-weight: 700;
line-height: 1.2;
.afl-row--detailed .afl-col-time .afl-text-time {
text-align: center !important;
}
.afl-departs__op-number {
font-size: 2.5vh;
font-weight: 400;
color: #cce0ff;
margin-top: 0.5vh;
.afl-row--compact .afl-col-time {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 13.5vw !important;
min-width: 13.5vw !important;
max-width: 13.5vw !important;
margin-right: 2.5vw !important;
}
.afl-departs__col-destination {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 2vw;
padding-right: 2vw;
padding-top: 0;
overflow: hidden;
.afl-row--compact .afl-col-time .afl-text-time {
text-align: center !important;
}
.afl-departs__dest-line {
display: flex;
align-items: center;
gap: 1vw;
margin-bottom: 0.5vh;
/* Colonne 2 : Type de train / Opérateur & Numéro */
.afl-col-train {
width: 15vw !important;
min-width: 15vw !important;
max-width: 15vw !important;
margin-right: 2.5vw !important;
padding: 0 !important;
}
.afl-departs__destination-name {
font-size: 6vh;
font-weight: 700;
line-height: 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -1px;
/* Colonne 3 : Destination & Gares desservies */
.afl-col-dest {
flex: 1 !important;
overflow: hidden !important;
padding-right: 1.5vw !important;
}
.afl-departs__vias-wrapper {
display: flex;
align-items: center;
width: 100%;
/* Colonne 4 : Voie */
.afl-col-voie {
width: 9.5vw !important;
min-width: 9.5vw !important;
max-width: 9.5vw !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.afl-departs__vias-static {
font-size: 3.5vh;
font-weight: 400;
color: #cce0ff;
white-space: pre;
/* ==========================================================================
PARTIE HAUTE (LIGNES ETENDUES)
========================================================================== */
.afl-top-row {
height: 8.5vh !important;
display: flex !important;
align-items: flex-end !important;
width: 100% !important;
overflow: hidden !important;
}
.afl-departs__vias-container {
flex: 1;
overflow: hidden;
white-space: nowrap;
position: relative;
/* Heure */
.afl-text-time {
font-size: 8.0vh !important;
font-weight: 800 !important;
color: #ffff00 !important;
line-height: 1 !important;
letter-spacing: -0.08vw !important;
margin: 0 !important;
padding: 0 !important;
}
.afl-departs__vias {
font-size: 3.5vh;
font-weight: 400;
color: #cce0ff;
display: inline-block;
white-space: nowrap;
animation: afl-marquee 6.6s linear infinite;
padding-left: 100%;
.afl-row--compact .afl-text-time {
font-size: 8.0vh !important;
display: flex !important;
align-items: center !important;
}
@keyframes afl-marquee {
0% { transform: translate(0, 0); }
100% { transform: translate(-100%, 0); }
/* Opérateur */
.afl-text-op {
font-size: 5.0vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
text-transform: uppercase !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 100% !important;
margin: 0 !important;
padding-bottom: 0.3vh !important;
}
.afl-departs__col-platform {
width: 12vw;
flex-shrink: 0;
display: flex;
justify-content: flex-end;
padding-top: 0;
/* Destination */
.afl-text-dest {
font-size: 8.0vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
white-space: nowrap !important;
margin: 0 !important;
padding: 0 !important;
}
.afl-departs__platform-box {
background-color: white;
border-radius: 0.8vh;
width: 10vw;
height: 12.5vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0.5vh 1vh rgba(0,0,0,0.2);
.afl-row--compact .afl-text-dest {
font-size: 7.5vh !important;
display: flex !important;
align-items: center !important;
}
.afl-departs__platform-label {
color: #012a6a; /* Matches background */
font-size: 2.8vh;
font-weight: 700;
line-height: 1.2;
/* ==========================================================================
PARTIE BASSE (LIGNES ETENDUES)
========================================================================== */
.afl-bottom-row {
height: 7.5vh !important;
display: flex !important;
align-items: center !important;
margin-top: 1.0vh !important;
width: 100% !important;
overflow: hidden !important;
}
.afl-departs__platform-value {
color: #012a6a; /* Matches background */
font-size: 6.8vh;
font-weight: 700;
line-height: 1.1;
/* Numéro de train */
.afl-text-num {
font-size: 4.8vh !important;
font-weight: 400 !important;
color: #ffffff !important;
line-height: 1 !important;
margin: 0 !important;
}
.afl-departs__separator-wrapper {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2vh;
display: flex;
align-items: center;
justify-content: center;
.afl-row--compact .afl-text-num {
font-size: 5.2vh !important;
font-weight: 400 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
}
.afl-departs__separator {
position: absolute;
height: 0.4vh;
background-color: #005eb8;
width: 96vw;
border-radius: 0.2vh;
/* ==========================================================================
STATUS PILLS (DIMENSIONS EXACTES SELON L'IMAGE 1)
========================================================================== */
.afl-pill {
display: inline-flex !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 0.8vw !important;
padding: 0.5vh 1.4vw 0.5vh 0.6vw !important;
border-radius: 3.0vh !important;
height: 5.8vh !important;
margin: 0 !important;
}
.afl-departs__train-illustration {
position: absolute;
z-index: 2;
background-color: #012a6a; /* matches background */
padding: 0 1vw;
.afl-pill-text {
font-size: 3.6vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
.afl-departs__train-illustration svg {
display: block;
.afl-pill--ontime {
background-color: #005fa9 !important;
color: #ffffff !important;
}
body:has(.afl-departs__container) {
margin: 0;
padding: 0;
overflow: hidden;
.afl-pill--delayed {
background-color: #f8c898 !important;
color: #111111 !important;
}
.afl-departs__container * {
box-sizing: border-box;
.afl-pill--suppressed {
background-color: #ff5c73 !important;
color: #111111 !important;
}
.afl-departs__separator-wrapper {
position: relative;
width: 100%;
height: 10px;
display: flex;
align-items: center;
justify-content: center;
.afl-icon {
width: 4.0vh !important;
height: 4.0vh !important;
flex-shrink: 0 !important;
}
.afl-departs__separator {
position: absolute;
height: 4px;
background-color: #007ac3;
width: calc(100% - 48px);
border-radius: 2px;
/* ==========================================================================
GARES DESSERVIES (VIAS - 'VIA' FIXE & DEFILEMENT DES GARES ULTRA-ACCELERE)
========================================================================== */
.afl-vias-container {
display: flex !important;
align-items: baseline !important;
white-space: nowrap !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
.afl-departs__train-illustration {
position: absolute;
z-index: 2;
background-color: #01347d; /* match row background so it overlays the line */
padding: 0 10px;
.afl-vias-label {
font-size: 3.6vh !important;
font-weight: 400 !important;
color: #ffffff !important;
margin-right: 0.5vw !important;
flex-shrink: 0 !important;
vertical-align: baseline !important;
}
.afl-departs__row--compact {
height: 10.5vh;
align-items: center;
padding-top: 0;
padding-bottom: 0;
.afl-vias-scroll-wrapper {
flex: 1 !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
display: flex !important;
align-items: baseline !important;
}
.afl-departs__row--compact .afl-departs__platform-box {
height: 8vh;
.afl-vias-inner {
display: inline-block !important;
white-space: nowrap !important;
}
.afl-departs__row--compact .afl-departs__platform-label {
font-size: 1.8vh;
.afl-vias-inner--animating {
animation-name: afl-vias-marquee !important;
animation-timing-function: linear !important;
animation-iteration-count: infinite !important;
padding-left: 25% !important;
}
.afl-departs__row--compact .afl-departs__platform-value {
font-size: 4vh;
@keyframes afl-vias-marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
.afl-departs__row--compact .afl-departs__col-time-status,
.afl-departs__row--compact .afl-departs__col-train-info,
.afl-departs__row--compact .afl-departs__col-destination,
.afl-departs__row--compact .afl-departs__col-platform {
padding-top: 0;
.afl-vias-stop {
font-size: 5.5vh !important;
font-weight: 400 !important;
color: #ffffff !important;
line-height: 1 !important;
vertical-align: baseline !important;
}
.afl-vias-chevron {
color: #00c3ff !important;
font-weight: 800 !important;
margin: 0 0.4vw !important;
font-size: 5.5vh !important;
vertical-align: baseline !important;
}
/* ==========================================================================
VOIE BOX (PROPORTIONS ET DESIGN STRICTEMENT CONFORMES A L'IMAGE)
========================================================================== */
.afl-voie-box {
background-color: #ffffff !important;
border-radius: 1.8vh !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 0.2vh !important;
width: 8.5vw !important;
height: 15.2vh !important;
padding: 0.5vh 0 !important;
box-shadow: 0 0.4vh 1.0vh rgba(0, 0, 0, 0.25) !important;
}
.afl-voie-label {
color: #002d62 !important;
font-size: 3.5vh !important;
font-weight: 600 !important;
line-height: 1 !important;
letter-spacing: -0.02vw !important;
margin: 0 !important;
}
.afl-voie-value {
color: #002d62 !important;
font-size: 9.8vh !important;
font-weight: 800 !important;
line-height: 0.9 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.afl-voie-box--bus {
justify-content: center !important;
align-items: center !important;
padding: 0 !important;
}
.afl-voie-box-compact {
background-color: #ffffff !important;
border-radius: 1.0vh !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 6.2vw !important;
height: 7.6vh !important;
padding: 0 !important;
box-shadow: 0 0.2vh 0.5vh rgba(0, 0, 0, 0.25) !important;
}
+328 -226
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import './AFLDeparts.css';
import supabase from '../../../lib/supabase';
import { fetchAllRows } from '../../../lib/fetchAllRows';
@@ -25,6 +25,74 @@ const formatTimeNoSeconds = (v) => {
return String(v).slice(0,5).replace(':','h');
};
const BusIcon = ({ size = '5.5vh', color = 'white', style = {} }) => (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={color}
style={{ flexShrink: 0, ...style }}
>
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
</svg>
);
const ViasMarquee = ({ viaNames }) => {
const containerRef = useRef(null);
const textRef = useRef(null);
const [isOverflowing, setIsOverflowing] = useState(false);
const [duration, setDuration] = useState(4);
useEffect(() => {
const check = () => {
if (containerRef.current && textRef.current) {
const cWidth = containerRef.current.clientWidth;
const tWidth = textRef.current.scrollWidth;
if (tWidth > cWidth) {
setIsOverflowing(true);
// Vitesse de défilement multipliée par 2 (~280px/s)
setDuration(Math.max(1.0, tWidth / 280));
} else {
setIsOverflowing(false);
}
}
};
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, [viaNames]);
if (!viaNames || viaNames.length === 0) {
return (
<div className="afl-vias-container">
<span className="afl-vias-stop">Direct</span>
</div>
);
}
return (
<div className="afl-vias-container">
<span className="afl-vias-label">via</span>
<div className="afl-vias-scroll-wrapper" ref={containerRef}>
<div
ref={textRef}
className={`afl-vias-inner ${isOverflowing ? 'afl-vias-inner--animating' : ''}`}
style={{
animationDuration: isOverflowing ? `${duration}s` : undefined
}}
>
{viaNames.map((v, i) => (
<React.Fragment key={i}>
{i > 0 && <span className="afl-vias-chevron">&gt;</span>}
<span className="afl-vias-stop">{v}</span>
</React.Fragment>
))}
</div>
</div>
</div>
);
};
// (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)
@@ -37,14 +105,10 @@ function Clock(){
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
return (
<div className="aff-classiques-departs__clock-container" aria-hidden>
<div className="aff-classiques-departs__clock-box">
<div className="aff-classiques-departs__clock-main">{hh}:{mm}</div>
<div className="aff-classiques-departs__clock-seconds">{ss}</div>
</div>
<div className="afl-clock-container" aria-hidden>
<span className="afl-clock-main">{hh}:{mm}</span>
</div>
);
}
@@ -58,6 +122,12 @@ export default function AFLDeparts(){
// gérer les +, décoder et remplacer les tirets par des espaces
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
// Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique
const pageParam = params.get('page');
const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10))
? Math.max(0, parseInt(pageParam, 10) - 1)
: null;
// debug flag activable via ?debug=1 or ?debug=true
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
const debug = useCallback((...args) => { if (debugEnabled) console.debug(...args); }, [debugEnabled]);
@@ -79,9 +149,10 @@ export default function AFLDeparts(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
// eslint-disable-next-line no-unused-vars
const [stationType, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setTravauxSubstitutions] = useState([]);
const [travauxSubstitutions, setTravauxSubstitutions] = useState([]);
const [, setLoading] = useState(false);
const [, setError] = useState(null);
// debug des données brutes renvoyées par Supabase
@@ -248,6 +319,7 @@ export default function AFLDeparts(){
}, []);
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
@@ -259,6 +331,7 @@ export default function AFLDeparts(){
}, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
// eslint-disable-next-line no-unused-vars
const thresholdForStationType = useCallback((type) => {
if (!type) return 30;
const t = String(type).trim().toLowerCase();
@@ -955,8 +1028,8 @@ export default function AFLDeparts(){
// Debug: log first few annotated horaires
try { debug('annotated horaires sample', annotated.slice(0,6).map(a => ({ id: a.id, numero: a.numero_train || a.numero, _status: a._status, _displayTime: a._displayTime }))); } catch (e) {}
// afficher seulement les 9 premières lignes sur l'afficheur
setHoraires((annotated || []).slice(0, 9));
// Enregistrer tous les horaires (sans limitation à 9 pour alimenter toutes les pages)
setHoraires(annotated || []);
setPerturbations(filteredPerturb || []);
setTravaux(travauxData || []);
setTravauxSubstitutions(travauxSubstData || []);
@@ -980,14 +1053,14 @@ export default function AFLDeparts(){
// Helpers pour perturbations (memoized plus haut)
const [pageIndex, setPageIndex] = useState(0);
const [showAlternateStatus, setShowAlternateStatus] = useState(false);
const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0);
const [alternateDelay, setAlternateDelay] = useState(false);
useEffect(() => {
const alternateInterval = setInterval(() => {
setShowAlternateStatus(prev => !prev);
const delayTimer = setInterval(() => {
setAlternateDelay(prev => !prev);
}, 2000);
return () => clearInterval(alternateInterval);
return () => clearInterval(delayTimer);
}, []);
const getPageBounds = useCallback((pIndex) => {
@@ -996,9 +1069,13 @@ export default function AFLDeparts(){
return { start, end: start + 8 };
}, []);
const totalPages = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8));
useEffect(() => {
if (lockedPageIndex !== null) {
setPageIndex(Math.min(lockedPageIndex, totalPages - 1));
return;
}
const pageInterval = setInterval(() => {
setPageIndex((prev) => {
const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8);
@@ -1007,241 +1084,266 @@ export default function AFLDeparts(){
});
}, 10000); // Change page every 10 seconds
return () => clearInterval(pageInterval);
}, [horaires.length]);
}, [horaires.length, lockedPageIndex, totalPages]);
const { start, end } = getPageBounds(pageIndex);
const displayedHoraires = horaires.slice(start, end);
const isPage1 = pageIndex === 0;
return (
<div className="afl-departs__container">
{/* HEADER */}
<div className="afl-departs__header">
<div className="afl-departs__header-left">
<div className="afl-departs__logo-wrapper">
<svg viewBox="0 0 24 24" width="4vh" height="4vh" fill="white">
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
</svg>
</div>
<div className="afl-departs__title">Départs</div>
</div>
<div className="afl-container">
{/* HEADER */}
<header className="afl-header">
<div className="afl-header-left">
<svg width="4.5vh" height="4.5vh" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
{/* Chevrons >> */}
<path d="M3 8l4 6-4 6M8 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
{/* Silhouette tête */}
<circle cx="17.5" cy="7" r="2.2" fill="white" />
{/* Silhouette corps + marche */}
<path d="M15.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
{/* Porte de train verticale */}
<path d="M23 4v20M23 4h3M23 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="afl-header-title">Départs</div>
</div>
<div className="afl-departs__header-center">
{totalPages > 1 && Array.from({ length: totalPages }).map((_, i) => (
<div key={i} className={`afl-departs__page-dot ${i === pageIndex ? 'afl-departs__page-dot--active' : ''}`}>
<div className="afl-header-center">
{totalPages > 1 && Array.from({ length: totalPages }).map((_, i) => (
<div key={i} className={`afl-page-dot ${i === pageIndex ? 'afl-page-dot--active' : ''}`}>
{i + 1}
</div>
))}
</div>
</div>
<div className="afl-departs__header-right">
<Clock />
</div>
</div>
<div className="afl-header-right">
<Clock />
</div>
</header>
{/* LISTE DES TRAINS */}
<ul className="afl-departs__list">
{displayedHoraires.length === 0 ? (
<li className="afl-departs__row afl-departs__row--empty" key="empty">
<div style={{ color: 'white', padding: 24, fontSize: 24 }}>Aucun horaire disponible.</div>
</li>
) : displayedHoraires.map((h, idx) => {
const horaire = h || {};
const op = (horaire.type_train || horaire.op || '').toUpperCase();
const number = horaire.numero_train || horaire.number || horaire.numero || '';
{/* ROWS */}
<ul className="afl-list">
{displayedHoraires.length === 0 ? (
<li className="afl-row afl-row--detailed" key="empty">
<div style={{ color: 'white', padding: 24, fontSize: 24 }}>Aucun horaire disponible.</div>
</li>
) : displayedHoraires.map((h, idx) => {
const horaire = h || {};
const isDetailed = isPage1 && idx < 4;
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || '';
const platform = (horaire._displayPlatform || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '').toString().trim();
const op = (horaire.type_train || horaire.op || horaire.type || '').toUpperCase();
const number = horaire.numero_train || horaire.number || horaire.numero || '';
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null);
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const rawPlatform = (
horaire._displayPlatform ||
horaire.quai_reel ||
horaire.quai ||
horaire.quai_prevu ||
horaire.platform ||
horaire.voie ||
horaire.track ||
''
).toString().trim();
// Removed time = '' to stop alternation
const isCG = rawPlatform.toUpperCase() === 'CG' ||
rawPlatform.toUpperCase().includes('CG') ||
String(horaire.quai || '').toUpperCase().includes('CG') ||
String(horaire.quai_reel || '').toUpperCase().includes('CG') ||
String(horaire.quai_prevu || '').toUpperCase().includes('CG') ||
String(horaire.platform || '').toUpperCase().includes('CG');
let showPlatform = !(status && status.label === 'supprimé') && Boolean(platform);
if (showPlatform) {
const threshold = thresholdForStationType(stationType);
let diffMinutes = null;
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
if (sched !== null) {
let baseDate = new Date();
if (horaire._date) {
const parts = horaire._date.split('-');
if (parts.length === 3) {
baseDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
}
}
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
if (!horaire._date) {
if (sched < 240 && nowHM > 1200) baseDate.setDate(baseDate.getDate() + 1);
else if (sched > 1200 && nowHM < 240) baseDate.setDate(baseDate.getDate() - 1);
}
diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
}
if (diffMinutes !== null) {
showPlatform = (diffMinutes <= threshold);
} else {
showPlatform = false;
}
}
const isCarTER = isCG ||
op.includes('CAR') ||
op.includes('AUTOCAR') ||
op.includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('BUS') ||
String(horaire.mode || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('CAR') ||
String(horaire.mode_transport || '').toUpperCase().includes('BUS') ||
String(horaire.type_train || '').toUpperCase().includes('CAR') ||
String(horaire.type_train || '').toUpperCase().includes('BUS') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') ||
String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') ||
(travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
// Calcul des vias
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
let isAfterStart = false;
for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
if (!sName) continue;
const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) {
isAfterStart = true;
continue;
}
if (isAfterStart) {
if (norm === normalizeName(destination || '')) {
break;
}
viaNames.push(sName);
}
}
}
const viasStr = viaNames.length > 0 ? viaNames.join(' > ') : '';
const marqueeStyle = viasStr.length > 0 ? { animationDuration: `${Math.max(6, viasStr.length * 0.25)}s` } : {};
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || '';
const isDelayed = status.label && String(status.label).toLowerCase().includes('retard');
const isSuppressed = status.label && String(status.label).toLowerCase().includes('supprimé');
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
const stationName = (gareDisplay) ? gareDisplay : (horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null);
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
let displayStatusText = "À l'heure";
let delayVal = '';
if (isDelayed) {
displayStatusText = status.label;
if (typeof status.delayMinutes === 'number') {
delayVal = `+${status.delayMinutes} min`;
displayStatusText = delayVal;
const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé');
let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG;
const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard');
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
let delayVal = '+ 5 min';
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
delayVal = `+ ${status.delayMinutes} min`;
} else if (status?.delayMinutes) {
delayVal = `+ ${status.delayMinutes} min`;
} else if (status?.label && status.label.includes('+')) {
delayVal = status.label;
}
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
// Calcul des vias
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
let isAfterStart = false;
for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
if (!sName) continue;
const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) {
isAfterStart = true;
continue;
}
if (isAfterStart) {
if (norm === normalizeName(destination || '')) {
break;
}
viaNames.push(sName);
}
}
}
if (isSuppressed) displayStatusText = "Supprimé";
const isBus = op.includes('CAR') || op.includes('AUTOCAR') || op.includes('BUS') || (horaire.mode && horaire.mode.toLowerCase().includes('bus'));
const renderPillIcon = () => {
if (isSuppressed) {
return (
<svg viewBox="0 0 20 20" className="afl-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<path d="M6.5 6.5l7 7M13.5 6.5l-7 7" stroke="#ff5c73" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
}
if (isDelayed) {
return (
<svg viewBox="0 0 20 20" className="afl-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<line x1="10" y1="4.5" x2="10" y2="11.5" stroke="white" strokeWidth="2.4" strokeLinecap="round" />
<circle cx="10" cy="14.8" r="1.4" fill="white" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" className="afl-icon">
<circle cx="10" cy="10" r="9.5" fill="white" />
<path d="M6 10.2l2.6 2.6 5.4-5.4" stroke="#005fa9" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const isCompact = pageIndex > 0 && idx > 0;
const showCompactStatus = isCompact;
return (
<li key={idx} className={`afl-row ${isDetailed ? 'afl-row--detailed' : 'afl-row--compact'}`}>
return (
<React.Fragment key={idx}>
<li className={`afl-departs__row ${isCompact ? 'afl-departs__row--compact' : ''}`}>
<div className="afl-departs__col-time-status">
{showCompactStatus && showAlternateStatus ? (
<div className={`afl-departs__status-pill ${isSuppressed ? 'afl-departs__status-pill--suppressed' : (isDelayed ? 'afl-departs__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#005eb8" />
</svg>
)}
<span>{displayStatusText}</span>
{/* TIME & PILL */}
<div className="afl-col-time">
{isDetailed ? (
<>
<div className="afl-top-row">
<div className="afl-text-time">{time}</div>
</div>
) : (
<>
<div className="afl-departs__time">{time}</div>
{!isCompact && (
<div className={`afl-departs__status-pill ${isSuppressed ? 'afl-departs__status-pill--suppressed' : (isDelayed ? 'afl-departs__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#005eb8" />
</svg>
)}
<span>{displayStatusText}</span>
</div>
)}
</>
)}
</div>
<div className="afl-departs__col-train-info">
<div className="afl-departs__op-name">{op}</div>
{!isCompact && <div className="afl-departs__op-number">{number}</div>}
</div>
<div className="afl-departs__col-destination">
<div className="afl-departs__dest-line">
{isBus && (
<svg viewBox="0 0 24 24" width="28" height="28" fill="white">
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z"/>
</svg>
)}
<MarqueeText text={destination} className="afl-departs__destination-name" />
</div>
{!isCompact && (
<div className="afl-departs__vias-wrapper">
{viasStr ? (
<>
<span className="afl-departs__vias-static">via </span>
<div className="afl-departs__vias-container">
<div className="afl-departs__vias" style={marqueeStyle}>{viasStr}</div>
</div>
</>
) : (
<span className="afl-departs__vias-static">direct</span>
)}
</div>
)}
</div>
<div className="afl-departs__col-platform">
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="8vh" height="8vh">
<circle cx="12" cy="12" r="11" fill="#ff4c5b" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="white" />
</svg>
) : showPlatform ? (
<div className="afl-departs__platform-box">
{!isBus && <div className="afl-departs__platform-label">Voie</div>}
<div className="afl-departs__platform-value">
{isBus ? (
<svg viewBox="0 0 24 24" width="7vh" height="7vh" fill="#012a6a">
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z"/>
</svg>
) : platform}
<div className="afl-bottom-row">
<div className={`afl-pill ${pillClass}`}>
{renderPillIcon()}
<span className="afl-pill-text">{pillText}</span>
</div>
</div>
) : null}
</div>
</li>
{/* Séparateur pour toutes les lignes sauf la dernière. */}
{idx < displayedHoraires.length - 1 && (
<div className="afl-departs__separator-wrapper">
<div className="afl-departs__separator" />
</div>
)}
</React.Fragment>
);
})}
</ul>
</div>
</>
) : (
alternateDelay ? (
<div className={`afl-pill ${pillClass}`}>
{renderPillIcon()}
<span className="afl-pill-text">{pillText}</span>
</div>
) : (
<div className="afl-text-time">{time}</div>
)
)}
</div>
{/* OP & NUMBER */}
<div className="afl-col-train">
{isDetailed ? (
<>
<div className="afl-top-row">
<div className="afl-text-op">{op}</div>
</div>
<div className="afl-bottom-row">
<div className="afl-text-num">{number}</div>
</div>
</>
) : (
alternateDelay ? (
<div className="afl-text-num">{op || number}</div>
) : (
<div className="afl-text-num">{number || op}</div>
)
)}
</div>
{/* DESTINATION & VIAS */}
<div className="afl-col-dest">
{isDetailed ? (
<>
<div className="afl-top-row">
{isCarTER && <BusIcon size="5.5vh" color="white" style={{ marginRight: '0.8vw' }} />}
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
</div>
<div className="afl-bottom-row">
<ViasMarquee viaNames={viaNames} />
</div>
</>
) : (
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
{isCarTER && <BusIcon size="5.0vh" color="white" style={{ marginRight: '0.8vw' }} />}
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
</div>
)}
</div>
{/* VOIE */}
<div className="afl-col-voie">
{isDetailed ? (
isSuppressed ? (
<svg viewBox="0 0 24 24" width="8.5vh" height="8.5vh" style={{ flexShrink: 0 }}>
<circle cx="12" cy="12" r="11" fill="#ff5c73" />
<path d="M8 8l8 8M16 8l-8 8" stroke="#002d62" strokeWidth="3.2" strokeLinecap="round" />
</svg>
) : isCarTER ? (
<div className="afl-voie-box afl-voie-box--bus">
<BusIcon size="9.5vh" color="#002d62" />
</div>
) : showPlatform ? (
<div className="afl-voie-box">
<div className="afl-voie-label">Voie</div>
<div className="afl-voie-value">{rawPlatform}</div>
</div>
) : null
) : (
isSuppressed ? (
<svg viewBox="0 0 24 24" width="6.0vh" height="6.0vh" style={{ flexShrink: 0 }}>
<circle cx="12" cy="12" r="11" fill="#ff5c73" />
<path d="M8 8l8 8M16 8l-8 8" stroke="#002d62" strokeWidth="3.2" strokeLinecap="round" />
</svg>
) : isCarTER ? (
<div className="afl-voie-box-compact">
<BusIcon size="5.2vh" color="#002d62" />
</div>
) : null
)}
</div>
</li>
);
})}
</ul>
</div>
);
}
+239 -275
View File
@@ -1,335 +1,299 @@
/* Déclaration des fontes Achemine (dans public/fonts/) */
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Achemine';
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
/* ==========================================================================
AFL DEPARTS QUAIS (ECRAN DE QUAI SNCF - PROCHAIN DEPART) - CSS
========================================================================== */
/* Conteneur Global Plein Ecran */
.afl-q-container {
font-family: 'Achemine', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
background: radial-gradient(circle at 60% 35%, #151a4a 0%, #0c0f2e 100%) !important;
background-color: #0c0f2e !important;
color: #ffffff !important;
width: 100vw !important;
height: 100vh !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
user-select: none !important;
z-index: 10000 !important;
}
.afl-quais-departs__container {
--bg-color: #042054; /* Deep dark blue background */
--header-bg: #083c84; /* Lighter vibrant blue header */
--accent-cyan: #00c3e3; /* Cyan timeline color */
--accent-yellow: #cce815; /* Bright chartreuse/lime-yellow for departure time */
--text-grey: #a0b2d6; /* Light grey-blue for train info */
.afl-q-container * {
box-sizing: border-box !important;
}
.afl-quais-departs__container {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
background-color: var(--bg-color);
color: white;
font-family: 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
overflow: hidden;
position: relative;
/* ==========================================================================
HEADER (10.5vh)
========================================================================== */
.afl-q-header {
height: 10.5vh !important;
min-height: 10.5vh !important;
max-height: 10.5vh !important;
background-color: #38466e !important; /* Couleur bleu ardoise exacte */
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 2.5vw !important;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.12) !important;
z-index: 10 !important;
}
/* --- HEADER --- */
.afl-quais-departs__header {
height: 12vh;
background-color: var(--header-bg);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 3vw;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
box-sizing: border-box;
.afl-q-header-left {
display: flex !important;
align-items: center !important;
gap: 1.2vw !important;
}
.afl-quais-departs__header-left {
display: flex;
align-items: center;
gap: 1.2vw;
.afl-q-header-icon {
width: 5.2vh !important;
height: 5.2vh !important;
flex-shrink: 0 !important;
}
.afl-quais-departs__header-title {
font-size: 5vh;
font-weight: 700;
letter-spacing: -0.02em;
.afl-q-header-title {
font-size: 5.0vh !important;
font-weight: 700 !important;
color: #ffffff !important;
letter-spacing: -0.03vw !important;
line-height: 1 !important;
}
.afl-quais-departs__header-clock {
font-size: 5vh;
font-weight: 700;
.afl-q-header-clock {
font-size: 5.0vh !important;
font-weight: 700 !important;
color: #ffffff !important;
letter-spacing: -0.03vw !important;
line-height: 1 !important;
}
/* --- MAIN CONTENT --- */
.afl-quais-departs__main {
flex: 1;
display: flex;
position: relative;
box-sizing: border-box;
/* ==========================================================================
CORPS PRINCIPAL (75vh)
========================================================================== */
.afl-q-main {
flex: 1 !important;
display: flex !important;
flex-direction: row !important;
padding: 2.0vh 2.5vw 0.5vh 2.5vw !important;
gap: 3.5vw !important;
overflow: hidden !important;
position: relative !important;
}
/* --- LEFT PANE (GLUED TO LEFT/TOP, ROUNDED BOTTOM RIGHT) --- */
.afl-quais-departs__left-pane {
position: absolute;
top: 0;
left: 0;
width: 35vw;
height: 38vh;
background: rgba(255, 255, 255, 0.07);
border-radius: 0 0 24px 0; /* Only bottom-right is rounded */
padding: 4vh 3vw;
display: flex;
flex-direction: column;
box-sizing: border-box;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-right: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
/* --------------------------------------------------------------------------
CORPS PRINCIPAL (MAIN)
-------------------------------------------------------------------------- */
.afl-q-main {
flex: 1 !important;
display: flex !important;
align-items: flex-start !important; /* Hauteur dynamique du panneau gauche */
padding: 2.5vh 2.5vw 1.5vh 2.5vw !important;
gap: 3.5vw !important;
overflow: hidden !important;
position: relative !important;
}
.afl-quais-departs__time-status {
display: flex;
align-items: center;
gap: 1vw;
margin-bottom: 1.5vh;
/* --------------------------------------------------------------------------
COLONNE GAUCHE (CARTE DU TRAIN - HAUTEUR DYNAMIQUE)
-------------------------------------------------------------------------- */
.afl-q-left-card {
width: 29.5vw !important;
min-width: 29.5vw !important;
max-width: 29.5vw !important;
height: auto !important; /* Dynamique selon le nombre de destinations */
background-color: rgba(32, 42, 78, 0.90) !important;
border: 1.5px solid rgba(255, 255, 255, 0.14) !important;
border-radius: 1.4vh !important;
padding: 1.8vh 1.6vw !important;
display: flex !important;
flex-direction: column !important;
gap: 1.4vh !important;
box-shadow: 0 0.8vh 2.0vh rgba(0, 0, 0, 0.35) !important;
position: relative !important;
}
.afl-quais-departs__time {
font-size: 7.5vh;
font-weight: 800;
color: var(--accent-yellow);
line-height: 1;
/* En-tête de carte : Heure & Badge Statut */
.afl-q-card-top {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding-bottom: 1.4vh !important;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.14) !important;
}
.afl-quais-departs__status-pill {
display: inline-flex;
align-items: center;
background-color: #005eb8;
border-radius: 3vh;
padding: 0.6vh 0.8vw;
font-size: 2.2vh;
font-weight: 600;
color: white;
width: max-content;
.afl-q-card-time {
font-size: 5.6vh !important;
font-weight: 800 !important;
color: #ffff00 !important; /* Même couleur jaune que sur AFLDeparts.js */
line-height: 1 !important;
letter-spacing: -0.04vw !important;
}
.afl-quais-departs__status-pill svg {
margin-right: 0.5vw;
/* Pastille Statut */
.afl-q-pill {
display: inline-flex !important;
align-items: center !important;
gap: 0.6vw !important;
padding: 0.5vh 1.0vw 0.5vh 0.7vw !important;
border-radius: 2.5vh !important;
background-color: #45527a !important;
font-size: 2.4vh !important;
font-weight: 700 !important;
color: #ffffff !important;
line-height: 1 !important;
}
.afl-quais-departs__status-pill--delayed {
background-color: #ff9e43;
color: #111;
.afl-q-pill--delayed {
background-color: #f8c898 !important;
color: #111111 !important;
}
.afl-quais-departs__status-pill--suppressed {
background-color: #ff4c5b;
color: #111;
.afl-q-pill--suppressed {
background-color: #ff5c73 !important;
color: #111111 !important;
}
.afl-quais-departs__destination {
font-size: 5.5vh;
font-weight: 700;
line-height: 1.15;
color: #ffffff;
margin-bottom: 1.5vh;
word-wrap: break-word;
.afl-q-pill-icon {
width: 2.6vh !important;
height: 2.6vh !important;
flex-shrink: 0 !important;
}
.afl-quais-departs__train-info {
font-size: 3vh;
color: var(--text-grey);
display: flex;
gap: 0.8vw;
/* Destinations & Numéros */
.afl-q-dest-block {
padding-bottom: 1.4vh !important;
border-bottom: 1.5px solid rgba(255, 255, 255, 0.14) !important;
display: flex !important;
flex-direction: column !important;
gap: 0.6vh !important;
}
.afl-quais-departs__train-type {
font-weight: 800;
.afl-q-dest-title {
font-size: 5.6vh !important; /* Grande police pour la gare de destination */
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1.05 !important;
letter-spacing: -0.03vw !important;
word-break: break-word !important;
}
.afl-quais-departs__train-number {
font-weight: 500;
.afl-q-dest-sub {
font-size: 2.6vh !important;
font-weight: 700 !important;
color: #9bb0d8 !important;
letter-spacing: 0.04vw !important;
text-transform: uppercase !important;
line-height: 1 !important;
margin-top: 0.4vh !important;
}
/* --- RIGHT PANE (TIMELINE) --- */
.afl-quais-departs__right-pane {
position: absolute;
top: 4vh;
left: 38vw;
width: 58vw;
bottom: 12vh;
display: flex;
flex-direction: column;
justify-content: center; /* Center stops list vertically */
box-sizing: border-box;
/* Bas de la carte : Logo du type de train */
.afl-q-card-footer {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
padding-top: 0.4vh !important;
}
.afl-quais-departs__stops-window {
width: 100%;
max-height: 100%;
overflow: hidden;
position: relative;
.afl-q-train-logo-box {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
max-height: 4.8vh !important;
}
.afl-quais-departs__stops-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
.afl-q-train-logo-img {
max-height: 4.5vh !important;
max-width: 15vw !important;
object-fit: contain !important;
}
.afl-quais-departs__stop-item {
display: flex;
align-items: center;
height: 6.2vh;
position: relative;
box-sizing: border-box;
.afl-q-train-logo-text {
border: 1.5px solid rgba(255, 255, 255, 0.85) !important;
border-radius: 0.6vh !important;
padding: 0.3vh 0.8vw !important;
font-size: 2.5vh !important;
font-style: italic !important;
font-weight: 800 !important;
color: #ffffff !important;
letter-spacing: 0.06vw !important;
}
/* Timeline Indicator (Line segment + Dot) */
.afl-quais-departs__stop-indicator {
position: relative;
width: 4vw;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
/* --------------------------------------------------------------------------
COLONNE DROITE (TIMELINE / PLAN DE LIGNE ARBORESCENT)
-------------------------------------------------------------------------- */
.afl-q-timeline-area {
flex: 1 !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
overflow: hidden !important;
position: relative !important;
}
.afl-quais-departs__stop-line {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 0.6vh;
transform: translateX(-50%);
background-color: var(--accent-cyan);
box-shadow: 0 0 6px rgba(0, 195, 227, 0.4);
z-index: 1;
.afl-q-timeline-svg {
width: 100% !important;
height: 100% !important;
overflow: visible !important;
}
/* Adjust line ends for first and last stops */
.afl-quais-departs__stop-item.is-first .afl-quais-departs__stop-line {
top: 50%;
/* ==========================================================================
FOOTER : BOX VOIE EN BAS À DROITE (SANS REPÈRES DE QUAI NI COMPOSITION)
========================================================================== */
.afl-q-footer {
height: 14vh !important;
min-height: 14vh !important;
max-height: 14vh !important;
display: flex !important;
align-items: flex-end !important;
justify-content: flex-end !important;
padding: 0 2.5vw 1.8vh 2.5vw !important;
position: relative !important;
z-index: 10 !important;
}
.afl-quais-departs__stop-item.is-last .afl-quais-departs__stop-line {
bottom: 50%;
/* --------------------------------------------------------------------------
BOX VOIE (BLANCHE / PROPORTIONS STRICTES)
-------------------------------------------------------------------------- */
.afl-q-voie-box {
background-color: #ffffff !important;
border-radius: 1.2vh !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
width: 7.2vw !important;
min-width: 7.2vw !important;
height: 12.5vh !important;
padding: 0.4vh 0 !important;
box-shadow: 0 0.6vh 1.5vh rgba(0, 0, 0, 0.35) !important;
flex-shrink: 0 !important;
}
/* Standard dot */
.afl-quais-departs__stop-dot {
width: 1.4vh;
height: 1.4vh;
border-radius: 50%;
background-color: var(--accent-cyan);
box-shadow: 0 0 6px rgba(0, 195, 227, 0.6);
z-index: 2;
position: relative;
.afl-q-voie-label {
color: #002447 !important;
font-size: 2.4vh !important;
font-weight: 600 !important;
line-height: 1 !important;
letter-spacing: -0.02vw !important;
margin: 0 !important;
}
/* Terminus dot */
.afl-quais-departs__stop-dot.is-terminus {
width: 2.6vh;
height: 2.6vh;
background-color: var(--bg-color); /* Hollow effect */
border: 0.6vh solid var(--accent-cyan);
box-shadow: 0 0 8px rgba(0, 195, 227, 0.8);
border-radius: 50%;
.afl-q-voie-value {
color: #002447 !important;
font-size: 6.8vh !important;
font-weight: 800 !important;
line-height: 0.9 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Stop Names */
.afl-quais-departs__stop-name {
font-size: 3.8vh;
font-weight: 500;
color: #ffffff;
padding-left: 1vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.afl-quais-departs__stop-name.is-terminus {
font-size: 4.5vh;
font-weight: 700;
color: #ffffff;
padding-left: 1vw;
}
/* --- BOTTOM TRAIN ILLUSTRATION --- */
.afl-quais-departs__bottom-container {
position: absolute;
bottom: 3.5vh;
left: 38vw;
width: 42vw;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.afl-quais-departs__train-illustration {
display: flex;
justify-content: center;
opacity: 0.85;
}
/* --- VOIE BOX (PLATFORM BOX) --- */
.afl-quais-departs__platform-box {
position: absolute;
bottom: 2vh;
right: 2vw;
background-color: #ffffff;
border-radius: 8px;
width: 6.5vw;
height: 13vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.35);
z-index: 10;
box-sizing: border-box;
}
.afl-quais-departs__platform-label {
font-size: 2.2vh;
color: var(--bg-color);
font-weight: 700;
text-transform: capitalize;
line-height: 1;
margin-bottom: 0.5vh;
}
.afl-quais-departs__platform-value {
font-size: 7vh;
color: var(--bg-color);
font-weight: 800;
line-height: 1;
}
/* --- NO DEPARTURE / LOADING STATE --- */
.afl-quais-departs__no-departure {
display: flex;
justify-content: center;
align-items: center;
height: 88vh;
font-size: 6vh;
font-weight: 600;
color: #a4c4e0;
}
+270 -171
View File
@@ -3,6 +3,7 @@ import supabase from '../../../lib/supabase';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLDepartsQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => {
@@ -667,7 +668,6 @@ const AFLDepartsQuais = () => {
const map = buildPerturbMap(filteredPerturb, parsedHoraires);
const annotated = applyPerturbationsToHoraires(finalList, filteredPerturb, null, gareDisplay, serviceDateStr, map);
// Now filter by platform specific to this quai
const candidatesOnPlatform = annotated.filter(h => {
const stopsList = getStops(h);
let stopForStation = null;
@@ -675,7 +675,8 @@ const AFLDepartsQuais = () => {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
if (!quaiParam) return true;
return platform.toLowerCase() === quaiParam.toLowerCase();
});
if (candidatesOnPlatform.length > 0) {
@@ -696,202 +697,300 @@ const AFLDepartsQuais = () => {
return () => clearInterval(interval);
}, [fetchData]);
if (loading) return <div className="afl-quais-departs__container" style={{background: '#042054'}}></div>;
if (!nextDeparture) return (
<div className="afl-quais-departs__container">
<div className="afl-quais-departs__header">
<div className="afl-quais-departs__header-left">
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
</svg>
<div className="afl-quais-departs__header-title">Prochain départ</div>
</div>
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
if (loading) return <div className="afl-q-container" style={{ background: '#0c0f2e' }}></div>;
if (!nextDeparture) {
return (
<div className="afl-q-container">
<header className="afl-q-header">
<div className="afl-q-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-header-icon">
<path d="M4 8l5 5-5 5M11 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="21" cy="11" r="2.6" fill="white" />
<path d="M19 15h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M26 6v20M26 6h2M26 26h2" stroke="white" strokeWidth="2.2" strokeLinecap="round" />
</svg>
<div className="afl-q-header-title">Prochain départ</div>
</div>
<div className="afl-q-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '4.0vh', color: '#ffffff', fontWeight: 600 }}>
Aucun départ prévu
</div>
<div className="afl-quais-departs__no-departure">Aucun départ prévu</div>
</div>
);
const stops = getStops(nextDeparture);
let filteredStops = [];
if (stops.length > 0 && gareDisplay) {
const stationIndex = stops.findIndex(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
if (stationIndex !== -1) {
filteredStops = stops.slice(stationIndex + 1);
} else {
const firstStopName = normalizeName((stops[0].gare_nom || stops[0].name || stops[0].nom || '').toString());
if (firstStopName && firstStopName === normalizeName(gareDisplay)) {
filteredStops = stops.slice(1);
}
}
<footer className="afl-q-footer">
{quaiParam && (
<div className="afl-q-voie-box">
<div className="afl-q-voie-label">Voie</div>
<div className="afl-q-voie-value">{quaiParam}</div>
</div>
)}
</footer>
</div>
);
}
const destName = nextDeparture.gare_arrivee_nom || nextDeparture.gare_arrivee || 'Terminus';
if (filteredStops.length > 0) {
const lastStopName = normalizeName((filteredStops[filteredStops.length - 1].gare_nom || filteredStops[filteredStops.length - 1].name || filteredStops[filteredStops.length - 1].nom || '').toString());
const normDest = normalizeName(destName);
if (lastStopName !== normDest && !lastStopName.includes(normDest) && !normDest.includes(lastStopName)) {
filteredStops.push({ gare_nom: destName, isLast: true });
} else {
filteredStops[filteredStops.length - 1].isLast = true;
}
} else {
filteredStops.push({ gare_nom: destName, isLast: true });
const trainData = nextDeparture;
const isSuppressed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('supprimé');
const isDelayed = trainData._status && String(trainData._status.label || '').toLowerCase().includes('retard');
let delayVal = '+ 5 min';
if (typeof trainData._status?.delayMinutes === 'number' && trainData._status.delayMinutes > 0) {
delayVal = `+ ${trainData._status.delayMinutes} min`;
} else if (trainData._status?.label && trainData._status.label.includes('+')) {
delayVal = trainData._status.label;
}
// Deduplicate stops by name
const seenStops = new Set();
filteredStops = filteredStops.filter(s => {
const name = normalizeName((s.gare_nom || s.name || s.nom || s.stop_name || '').toString());
if (!name) return false;
if (seenStops.has(name)) return false;
seenStops.add(name);
return true;
});
const pillClass = isSuppressed ? 'afl-q-pill--suppressed' : isDelayed ? 'afl-q-pill--delayed' : '';
const pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayVal : "À l'heure";
const isSuppressed = nextDeparture._status && nextDeparture._status.label === 'supprimé';
const isDelayed = nextDeparture._status && nextDeparture._status.label && String(nextDeparture._status.label).toLowerCase().includes('retard');
let displayStatusText = "À l'heure";
if (isSuppressed) displayStatusText = "Supprimé";
else if (isDelayed) {
if (typeof nextDeparture._status.delayMinutes === 'number') {
displayStatusText = `+${nextDeparture._status.delayMinutes} min`;
} else {
displayStatusText = nextDeparture._status.label;
const renderPillIcon = () => {
if (isSuppressed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<path d="M6.5 6.5l7 7M13.5 6.5l-7 7" stroke="#ff5c73" strokeWidth="2.5" strokeLinecap="round" />
</svg>
);
}
}
if (isDelayed) {
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="#1e1e1e" />
<line x1="10" y1="4.5" x2="10" y2="11.5" stroke="white" strokeWidth="2.4" strokeLinecap="round" />
<circle cx="10" cy="14.8" r="1.4" fill="white" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" className="afl-q-pill-icon">
<circle cx="10" cy="10" r="9.5" fill="white" />
<path d="M6 10.2l2.6 2.6 5.4-5.4" stroke="#45527a" strokeWidth="2.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
const displayTime = nextDeparture._displayTime || formatTimeNoSeconds(nextDeparture.heure_depart || nextDeparture.time || nextDeparture.heure || '');
const trainType = nextDeparture.type_train || nextDeparture.op || 'TER';
const trainNum = nextDeparture.numero_train || nextDeparture.numero || nextDeparture.number || '';
const displayTime = trainData._displayTime || formatTimeNoSeconds(trainData.heure_depart || trainData.time || trainData.heure || '');
const trainType = trainData.type_train || trainData.op || 'TER';
const trainNum = trainData.numero_train || trainData.numero || trainData.number || '';
const destName = trainData.gare_arrivee_nom || trainData.gare_arrivee || 'Terminus';
const stopsList = getStops(nextDeparture);
const stopsList = getStops(trainData);
let stopForStation = null;
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || nextDeparture.quai_reel || nextDeparture.voie || nextDeparture.quai || quaiParam || '').toString().trim();
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim();
const shouldScroll = filteredStops.length > 10;
// eslint-disable-next-line no-unused-vars
const scrollStyle = shouldScroll ? {
animation: `scrollStops ${filteredStops.length * 2.2}s linear infinite`
} : {};
// Extraction des destinations (prise en compte des trains multi-branches / tranches)
let destinations = [];
if (Array.isArray(trainData.tranches) && trainData.tranches.length > 0) {
destinations = trainData.tranches.map(t => ({
name: t.gare_arrivee_nom || t.destination || t.nom,
op: t.type_train || t.op || trainType,
number: t.numero_train || t.number || trainNum
}));
} else if (destName.includes('/')) {
destinations = destName.split('/').map(d => ({
name: d.trim(),
op: trainType,
number: trainNum
}));
} else if (destName.includes(' - ') && (destName.toLowerCase().includes('marseille') || destName.toLowerCase().includes('montpellier'))) {
destinations = [
{ name: 'Marseille Saint-Charles', op: trainType, number: trainNum },
{ name: 'Montpellier Saint-Roch', op: trainType, number: trainNum }
];
} else {
destinations = [{
name: destName,
op: trainType,
number: trainNum
}];
}
// Formatage des lignes de destinations (découpage élégant sur 2 lignes si nécessaire)
const formatDestLines = (name) => {
if (!name) return ['Terminus'];
const clean = name.trim();
if (clean.includes('Marseille Saint-Charles')) return ['Marseille Saint-', 'Charles'];
if (clean.includes('Montpellier Saint-Roch')) return ['Montpellier', 'Saint-Roch'];
if (clean.includes('Paris Gare de Lyon')) return ['Paris', 'Gare de Lyon'];
if (clean.includes('Paris Montparnasse')) return ['Paris', 'Montparnasse'];
if (clean.includes('Bourg-en-Bresse')) return ['Bourg-en-', 'Bresse'];
if (clean.length > 12) {
const parts = clean.split(' ');
if (parts.length >= 2) {
const mid = Math.ceil(parts.length / 2);
return [parts.slice(0, mid).join(' '), parts.slice(mid).join(' ')];
}
}
return [clean];
};
// Calcul dynamique des arrêts desservis (aval)
const currentStationName = gareDisplay || (stopForStation && (stopForStation.gare_nom || stopForStation.nom || stopForStation.name)) || (stopsList[0] && (stopsList[0].gare_nom || stopsList[0].nom || stopsList[0].name)) || 'Départ';
let downstreamStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) {
const normCurrent = normalizeName(currentStationName);
const startIdx = stopsList.findIndex(s => {
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
});
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList.slice(1);
const seen = new Set();
rawDownstream.forEach(s => {
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
const norm = normalizeName(name);
if (name && norm !== normCurrent && !seen.has(norm)) {
seen.add(norm);
downstreamStops.push(name);
}
});
}
// Si aucun arrêt intermédiaire n'est trouvé, afficher la/les destination(s)
if (downstreamStops.length === 0) {
if (destName) {
if (destName.includes('/')) {
downstreamStops = destName.split('/').map(d => d.trim());
} else {
downstreamStops = [destName];
}
}
}
return (
<div className="afl-quais-departs__container">
{/* Header */}
<div className="afl-quais-departs__header">
<div className="afl-quais-departs__header-left">
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
{/* Chevrons / motion lines */}
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
{/* Train icon */}
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
</svg>
<div className="afl-quais-departs__header-title">Prochain départ</div>
<div className="afl-q-container">
{/* HEADER (10.5vh) */}
<header className="afl-q-header">
<div className="afl-q-header-left">
<svg width="5.2vh" height="5.2vh" viewBox="0 0 32 32" fill="none" className="afl-q-header-icon">
<path d="M4 8l5 5-5 5M11 8l5 5-5 5" stroke="white" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="21" cy="11" r="2.6" fill="white" />
<path d="M19 15h4.5l1.6 5.8-2 1-1-3.6-1 5.8-3-1 1.6-8z" fill="white" />
<path d="M26 6v20M26 6h2M26 26h2" stroke="white" strokeWidth="2.2" strokeLinecap="round" />
</svg>
<div className="afl-q-header-title">Prochain départ</div>
</div>
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
</div>
<div className="afl-q-header-clock">{formatTimeNoSeconds(currentTime)}</div>
</header>
<div className="afl-quais-departs__main">
{/* Left Pane (Floating Card) */}
<div className="afl-quais-departs__left-pane">
<div className="afl-quais-departs__time-status">
<div className="afl-quais-departs__time">{displayTime}</div>
<div className={`afl-quais-departs__status-pill ${isSuppressed ? 'afl-quais-departs__status-pill--suppressed' : (isDelayed ? 'afl-quais-departs__status-pill--delayed' : '')}`}>
{isSuppressed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
</svg>
) : isDelayed ? (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="#111" />
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
</svg>
) : (
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
<circle cx="12" cy="12" r="11" fill="white" />
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#005eb8" />
</svg>
)}
<span>{displayStatusText}</span>
{/* CORPS PRINCIPAL */}
<main className="afl-q-main">
{/* PANNEAU GAUCHE : CARTE D'INFORMATION DU TRAIN */}
<section className="afl-q-left-card">
{/* Ligne Heure de départ & Statut */}
<div className="afl-q-card-top">
<div className="afl-q-card-time">{displayTime}</div>
<div className={`afl-q-pill ${pillClass}`}>
{renderPillIcon()}
<span>{pillText}</span>
</div>
</div>
<div className="afl-quais-departs__destination">{destName}</div>
<div className="afl-quais-departs__train-info">
<span className="afl-quais-departs__train-type">{trainType}</span>
<span className="afl-quais-departs__train-number">{trainNum}</span>
</div>
</div>
{/* Right Pane (Timeline) */}
<div className="afl-quais-departs__right-pane">
<div className="afl-quais-departs__stops-window">
<style>
{`
@keyframes scrollStops {
0%, 15% { transform: translateY(0); }
85%, 100% { transform: translateY(calc(-100% + 65vh)); }
}
`}
</style>
<ul className="afl-quais-departs__stops-list" style={shouldScroll ? { animation: `scrollStops ${filteredStops.length * 1.5}s ease-in-out infinite alternate` } : {}}>
{/* Main List */}
{filteredStops.map((s, i) => {
const sname = s.gare_nom || s.nom || s.name || s.stop_name || '';
const isFirst = !shouldScroll && i === 0;
const isLastItem = s.isLast || i === filteredStops.length - 1;
const isLast = !shouldScroll && isLastItem;
{/* Blocs Destinations dynamiques selon l'horaire */}
{destinations.map((d, i) => {
const lines = formatDestLines(d.name);
return (
<div key={i} className="afl-q-dest-block">
{lines.map((line, lIdx) => (
<div key={lIdx} className="afl-q-dest-title">{line}</div>
))}
<div className="afl-q-dest-sub">
{d.op} {d.number}
</div>
</div>
);
})}
{/* Pied de carte : Logo du type de train (sans icônes de services) */}
<div className="afl-q-card-footer">
<Logo
op={trainType}
classNameBox="afl-q-train-logo-box"
classNameImg="afl-q-train-logo-img"
classNameText="afl-q-train-logo-text"
/>
</div>
</section>
{/* PANNEAU DROIT : TIMELINE ARBORESCENTE DYNAMIQUE */}
<section className="afl-q-timeline-area">
<svg viewBox="0 0 850 560" className="afl-q-timeline-svg">
{/* 3 mini-points cyan au sommet */}
<circle cx="45" cy="22" r="4" fill="#00c5ff" />
<circle cx="45" cy="38" r="4" fill="#00c5ff" />
<circle cx="45" cy="54" r="4" fill="#00c5ff" />
{/* Station 1: Gare actuelle */}
<circle cx="45" cy="92" r="17" fill="#0c102a" stroke="#00c5ff" strokeWidth="4" />
<path d="M 37 89 L 45 97 L 53 89" stroke="#00c5ff" strokeWidth="3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<text x="82" y="102" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{currentStationName}
</text>
{/* Tracé dynamique des arrêts aval */}
{downstreamStops.length <= 1 ? (
<g>
<line x1="45" y1="109" x2="45" y2="480" stroke="#00c5ff" strokeWidth="5.5" strokeLinecap="round" />
<circle cx="45" cy="480" r="16" fill="#0c102a" stroke="#00c5ff" strokeWidth="5" />
<text x="82" y="490" fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{downstreamStops[0] || destName}
</text>
</g>
) : (
(() => {
const N = downstreamStops.length;
const yStart = 160;
const yEnd = 500;
const step = Math.min(65, (yEnd - yStart) / Math.max(1, N - 1));
const lineEndY = yStart + (N - 1) * step;
return (
<li key={`s1-${i}`} className={`afl-quais-departs__stop-item ${isFirst ? 'is-first' : ''} ${isLast ? 'is-last' : ''}`}>
<div className="afl-quais-departs__stop-indicator">
<div className="afl-quais-departs__stop-line"></div>
<div className={`afl-quais-departs__stop-dot ${isLastItem ? 'is-terminus' : ''}`}></div>
</div>
<div className={`afl-quais-departs__stop-name ${isLastItem ? 'is-terminus' : ''}`}>{sname}</div>
</li>
<g>
<line x1="45" y1="109" x2="45" y2={lineEndY} stroke="#00c5ff" strokeWidth="5.5" strokeLinecap="round" />
{downstreamStops.map((stopName, sIdx) => {
const isLast = sIdx === N - 1;
const posY = yStart + sIdx * step;
if (!isLast) {
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="6.5" fill="#00c5ff" />
<text x="82" y={posY + 8} fill="#ffffff" fontSize="28" fontWeight="500" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
}
return (
<g key={sIdx}>
<circle cx="45" cy={posY} r="16" fill="#0c102a" stroke="#00c5ff" strokeWidth="5" />
<text x="82" y={posY + 10} fill="#ffffff" fontSize="35" fontWeight="800" fontFamily="Achemine, Segoe UI, sans-serif">
{stopName}
</text>
</g>
);
})}
</g>
);
})}
</ul>
</div>
</div>
</div>
{/* Symmetrical train outline under the stops list */}
<div className="afl-quais-departs__bottom-container">
<div className="afl-quais-departs__train-illustration">
<svg viewBox="0 0 260 40" width="22vw" height="3.5vh">
<path fill="none" stroke="#00c3e3" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
d="M30,35 L230,35 C242,35 248,31 254,22 L258,12 C260,8 256,4 247,4 L13,4 C4,4 0,8 2,12 L6,22 C12,31 18,35 30,35 Z" />
<line x1="55" y1="4" x2="55" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="82" y1="4" x2="82" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="109" y1="4" x2="109" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="136" y1="4" x2="136" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="163" y1="4" x2="163" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="190" y1="4" x2="190" y2="35" stroke="#00c3e3" strokeWidth="2" />
<line x1="217" y1="4" x2="217" y2="35" stroke="#00c3e3" strokeWidth="2" />
})()
)}
</svg>
</div>
</div>
</section>
</main>
{platformStr && (
<div className="afl-quais-departs__platform-box">
<div className="afl-quais-departs__platform-label">Voie</div>
<div className="afl-quais-departs__platform-value">{platformStr}</div>
{/* FOOTER : BOX VOIE EN BAS À DROITE (SANS REPÈRES DE QUAI NI COMPOSITION) */}
<footer className="afl-q-footer">
<div className="afl-q-voie-box">
<div className="afl-q-voie-label">Voie</div>
<div className="afl-q-voie-value">{platformStr}</div>
</div>
)}
</footer>
</div>
);
};
+476
View File
@@ -0,0 +1,476 @@
/* ==========================================================================
EVA ARRIVEES (AFFICHEUR VOYAGEURS EVA) - CSS
========================================================================== */
/* Container Principal */
.eva-arr-container {
font-family: 'Avenir', 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
height: 100vh !important;
width: 100vw !important;
display: flex !important;
flex-direction: column !important;
background-color: #000824 !important; /* Bleu nuit très profond EVA */
overflow: hidden !important;
color: #ffffff !important;
margin: 0 !important;
padding: 1.2vh 1.8vw !important;
box-sizing: border-box !important;
user-select: none !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
/* ==========================================================================
HEADER (En-tête EVA)
========================================================================== */
.eva-arr-header {
height: 9.5vh !important;
min-height: 9.5vh !important;
max-height: 9.5vh !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 0.5vw !important;
margin-bottom: 1.0vh !important;
box-sizing: border-box !important;
}
.eva-arr-header-left {
display: flex !important;
align-items: center !important;
gap: 1.0vw !important;
}
.eva-arr-header-icon {
width: 5.5vh !important;
height: 5.5vh !important;
flex-shrink: 0 !important;
color: #00d68f !important;
}
.eva-arr-header-titles {
display: flex !important;
align-items: baseline !important;
gap: 0.6vw !important;
white-space: nowrap !important;
}
.eva-arr-header-title-main {
font-size: 5.2vh !important;
font-weight: 900 !important;
color: #ffffff !important;
letter-spacing: 0.04vw !important;
line-height: 1 !important;
text-transform: uppercase !important;
}
.eva-arr-header-slash {
font-size: 3.8vh !important;
font-weight: 700 !important;
color: #00d68f !important;
line-height: 1 !important;
}
.eva-arr-header-title-sub {
font-size: 3.2vh !important;
font-weight: 700 !important;
color: #00d68f !important;
letter-spacing: 0.02vw !important;
line-height: 1 !important;
text-transform: uppercase !important;
}
.eva-arr-header-right {
display: flex !important;
align-items: center !important;
}
/* Horloge EVA */
.eva-arr-clock-box {
border: 2.2px solid #00d68f !important;
background-color: #000824 !important;
border-radius: 8px !important;
padding: 0.5vh 1.0vw !important;
display: flex !important;
align-items: baseline !important;
gap: 0.5vw !important;
box-shadow: 0 0 10px rgba(0, 214, 143, 0.15) !important;
}
.eva-arr-clock-main {
font-size: 4.8vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: 0.02vw !important;
}
.eva-arr-clock-seconds {
font-size: 3.8vh !important;
font-weight: 800 !important;
color: #00d68f !important;
line-height: 1 !important;
}
/* ==========================================================================
LISTE DES HORAIRES (Cartes)
========================================================================== */
.eva-arr-list {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
gap: 1.2vh !important;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
overflow: hidden !important;
}
.eva-arr-row {
background-color: #e9eef6 !important;
border-radius: 10px !important;
display: flex !important;
align-items: stretch !important;
height: calc((100% - 3.6vh) / 4) !important;
min-height: 12vh !important;
box-sizing: border-box !important;
overflow: hidden !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
}
/* --------------------------------------------------------------------------
COLONNE GAUCHE : HEURE & STATUT
-------------------------------------------------------------------------- */
.eva-arr-col-time {
width: 14.5vw !important;
min-width: 14.5vw !important;
max-width: 14.5vw !important;
background-color: #d7e1f0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
padding: 0.8vh 0.6vw !important;
box-sizing: border-box !important;
border-top-left-radius: 10px !important;
border-bottom-left-radius: 10px !important;
}
.eva-arr-time-value {
font-size: 5.6vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1 !important;
margin-bottom: 0.6vh !important;
letter-spacing: -0.02vw !important;
}
/* Pastilles de statut */
.eva-arr-status-pill {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 0.4vw !important;
padding: 0.3vh 0.9vw !important;
border-radius: 20px !important;
box-sizing: border-box !important;
height: 3.8vh !important;
}
.eva-arr-status-pill--ontime {
background-color: #d7e1f0 !important;
border: 1.8px solid #7d95b5 !important;
color: #1a2b4c !important;
}
.eva-arr-status-pill--delayed {
background-color: #ffa74f !important;
color: #000c2f !important;
}
.eva-arr-status-pill--suppressed {
background-color: #ff5c73 !important;
color: #ffffff !important;
}
.eva-arr-status-icon {
width: 2.5vh !important;
height: 2.5vh !important;
flex-shrink: 0 !important;
}
.eva-arr-status-text {
font-size: 2.2vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
/* --------------------------------------------------------------------------
SECTION CENTRALE : PROVENANCE & GARES DESSERVIES (VIAS)
-------------------------------------------------------------------------- */
.eva-arr-col-center {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
padding: 0.6vh 1.8vw !important;
box-sizing: border-box !important;
overflow: hidden !important;
min-width: 0 !important;
}
.eva-arr-origin-row {
display: flex !important;
align-items: center !important;
gap: 0.8vw !important;
overflow: hidden !important;
white-space: nowrap !important;
margin-bottom: 0.4vh !important;
}
.eva-arr-origin-name {
font-size: 5.6vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1.1 !important;
letter-spacing: -0.02vw !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* Badge Motif de Perturbation (ex: Embouteillages) */
.eva-arr-perturb-badge {
display: inline-flex !important;
align-items: center !important;
gap: 0.4vw !important;
background-color: #ffa74f !important;
color: #000c2f !important;
padding: 0.4vh 0.8vw !important;
border-radius: 6px !important;
height: 3.4vh !important;
box-sizing: border-box !important;
flex-shrink: 0 !important;
}
.eva-arr-perturb-badge-icon {
width: 2.4vh !important;
height: 2.4vh !important;
flex-shrink: 0 !important;
}
.eva-arr-perturb-badge-text {
font-size: 2.2vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
/* Section Vias */
.eva-arr-vias-container {
display: flex !important;
align-items: baseline !important;
white-space: nowrap !important;
overflow: hidden !important;
width: 100% !important;
}
.eva-arr-vias-label {
font-size: 3.5vh !important;
font-weight: 700 !important;
color: #00c284 !important; /* Vert menthe SNCF */
margin-right: 0.5vw !important;
flex-shrink: 0 !important;
}
.eva-arr-vias-scroll-wrapper {
flex: 1 !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
display: flex !important;
align-items: baseline !important;
}
.eva-arr-vias-inner {
display: inline-flex !important;
align-items: baseline !important;
white-space: nowrap !important;
}
.eva-arr-vias-inner--animating {
animation-name: eva-arr-vias-marquee !important;
animation-timing-function: linear !important;
animation-iteration-count: infinite !important;
padding-left: 20% !important;
}
@keyframes eva-arr-vias-marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
.eva-arr-vias-stop {
font-size: 3.5vh !important;
font-weight: 500 !important;
color: #2c3b57 !important;
line-height: 1 !important;
}
.eva-arr-vias-bullet {
color: #00c284 !important;
font-weight: 800 !important;
margin: 0 0.5vw !important;
font-size: 2.8vh !important;
display: inline-block !important;
vertical-align: middle !important;
}
/* --------------------------------------------------------------------------
SECTION DROITE : MODE DE TRANSPORT / VOIE & OPERATEUR / NUMERO
-------------------------------------------------------------------------- */
.eva-arr-col-right {
display: flex !important;
align-items: center !important;
height: 100% !important;
flex-shrink: 0 !important;
}
/* Boîte Picto Autocar/Bus */
.eva-arr-bus-box {
background-color: #000c2f !important;
border-radius: 8px !important;
width: 7.0vw !important;
height: 80% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0.8vw !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
}
.eva-arr-bus-icon {
width: 6.0vh !important;
height: 6.0vh !important;
color: #ffffff !important;
}
/* Boîte Voie (si applicable pour train) */
.eva-arr-voie-box {
background-color: #000c2f !important;
border-radius: 8px !important;
width: 7.0vw !important;
height: 80% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0.8vw !important;
color: #ffffff !important;
}
.eva-arr-voie-label {
font-size: 1.8vh !important;
font-weight: 600 !important;
text-transform: uppercase !important;
opacity: 0.8 !important;
}
.eva-arr-voie-val {
font-size: 4.5vh !important;
font-weight: 900 !important;
line-height: 1 !important;
}
/* Bloc Opérateur & Numéro Vert Émeraude */
.eva-arr-train-block {
background-color: #00d68f !important;
width: 10.0vw !important;
min-width: 10.0vw !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
border-top-right-radius: 10px !important;
border-bottom-right-radius: 10px !important;
padding: 0 0.5vw !important;
box-sizing: border-box !important;
}
.eva-arr-train-op {
font-size: 2.8vh !important;
font-weight: 700 !important;
color: #000c2f !important;
line-height: 1 !important;
margin-bottom: 0.3vh !important;
text-transform: uppercase !important;
}
.eva-arr-train-num {
font-size: 4.2vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1 !important;
letter-spacing: 0.02vw !important;
}
/* ==========================================================================
BANDEAU TRAVAUX / ALERTES (Pied de page)
========================================================================== */
.eva-arr-bottom-alert {
background-color: #fca758 !important;
border-radius: 10px !important;
display: flex !important;
align-items: center !important;
padding: 0.8vh 1.8vw !important;
margin-top: 1.0vh !important;
gap: 1.4vw !important;
box-sizing: border-box !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
flex-shrink: 0 !important;
height: 10.8vh !important;
min-height: 10.8vh !important;
max-height: 10.8vh !important;
}
.eva-arr-bottom-alert-icon-wrapper {
flex-shrink: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.eva-arr-bottom-alert-icon {
width: 5.8vh !important;
height: 5.8vh !important;
color: #000c2f !important;
}
.eva-arr-bottom-alert-container {
flex: 1 !important;
overflow: hidden !important;
position: relative !important;
height: 9.0vh !important;
max-height: 9.0vh !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
}
.eva-arr-bottom-alert-content {
color: #000c2f !important;
font-size: 2.4vh !important;
font-weight: 600 !important;
line-height: 1.25 !important;
white-space: normal !important;
word-break: break-word !important;
will-change: transform, opacity !important;
}
File diff suppressed because it is too large. Load diff
+476
View File
@@ -0,0 +1,476 @@
/* ==========================================================================
EVA DEPARTS (AFFICHEUR VOYAGEURS EVA) - CSS
========================================================================== */
/* Container Principal */
.eva-dep-container {
font-family: 'Avenir', 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
height: 100vh !important;
width: 100vw !important;
display: flex !important;
flex-direction: column !important;
background-color: #000824 !important; /* Bleu nuit très profond EVA */
overflow: hidden !important;
color: #ffffff !important;
margin: 0 !important;
padding: 1.2vh 1.8vw !important;
box-sizing: border-box !important;
user-select: none !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
/* ==========================================================================
HEADER (En-tête EVA Départs)
========================================================================== */
.eva-dep-header {
height: 9.5vh !important;
min-height: 9.5vh !important;
max-height: 9.5vh !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 0.5vw !important;
margin-bottom: 1.0vh !important;
box-sizing: border-box !important;
}
.eva-dep-header-left {
display: flex !important;
align-items: center !important;
gap: 1.0vw !important;
}
.eva-dep-header-icon {
width: 5.5vh !important;
height: 5.5vh !important;
flex-shrink: 0 !important;
color: #03c5ff !important; /* Bleu cyan/azur vif EVA Départs (#03c5ff) */
}
.eva-dep-header-titles {
display: flex !important;
align-items: baseline !important;
gap: 0.6vw !important;
white-space: nowrap !important;
}
.eva-dep-header-title-main {
font-size: 5.2vh !important;
font-weight: 900 !important;
color: #ffffff !important;
letter-spacing: 0.04vw !important;
line-height: 1 !important;
text-transform: uppercase !important;
}
.eva-dep-header-slash {
font-size: 3.8vh !important;
font-weight: 700 !important;
color: #03c5ff !important;
line-height: 1 !important;
}
.eva-dep-header-title-sub {
font-size: 3.2vh !important;
font-weight: 700 !important;
color: #03c5ff !important;
letter-spacing: 0.02vw !important;
line-height: 1 !important;
text-transform: uppercase !important;
}
.eva-dep-header-right {
display: flex !important;
align-items: center !important;
}
/* Horloge EVA Départs */
.eva-dep-clock-box {
border: 2.2px solid #03c5ff !important;
background-color: #000824 !important;
border-radius: 8px !important;
padding: 0.5vh 1.0vw !important;
display: flex !important;
align-items: baseline !important;
gap: 0.5vw !important;
box-shadow: 0 0 12px rgba(3, 197, 255, 0.25) !important;
}
.eva-dep-clock-main {
font-size: 4.8vh !important;
font-weight: 800 !important;
color: #ffffff !important;
line-height: 1 !important;
letter-spacing: 0.02vw !important;
}
.eva-dep-clock-seconds {
font-size: 3.8vh !important;
font-weight: 800 !important;
color: #03c5ff !important;
line-height: 1 !important;
}
/* ==========================================================================
LISTE DES HORAIRES (Cartes Départs)
========================================================================== */
.eva-dep-list {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
gap: 1.2vh !important;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
overflow: hidden !important;
}
.eva-dep-row {
background-color: #e9eef6 !important;
border-radius: 10px !important;
display: flex !important;
align-items: stretch !important;
height: calc((100% - 3.6vh) / 4) !important;
min-height: 12vh !important;
box-sizing: border-box !important;
overflow: hidden !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
}
/* --------------------------------------------------------------------------
COLONNE GAUCHE : HEURE & STATUT
-------------------------------------------------------------------------- */
.eva-dep-col-time {
width: 14.5vw !important;
min-width: 14.5vw !important;
max-width: 14.5vw !important;
background-color: #d7e1f0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
padding: 0.8vh 0.6vw !important;
box-sizing: border-box !important;
border-top-left-radius: 10px !important;
border-bottom-left-radius: 10px !important;
}
.eva-dep-time-value {
font-size: 5.6vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1 !important;
margin-bottom: 0.6vh !important;
letter-spacing: -0.02vw !important;
}
/* Pastilles de statut */
.eva-dep-status-pill {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 0.4vw !important;
padding: 0.3vh 0.9vw !important;
border-radius: 20px !important;
box-sizing: border-box !important;
height: 3.8vh !important;
}
.eva-dep-status-pill--ontime {
background-color: #d7e1f0 !important;
border: 1.8px solid #7d95b5 !important;
color: #1a2b4c !important;
}
.eva-dep-status-pill--delayed {
background-color: #ffa74f !important;
color: #000c2f !important;
}
.eva-dep-status-pill--suppressed {
background-color: #ff5c73 !important;
color: #ffffff !important;
}
.eva-dep-status-icon {
width: 2.5vh !important;
height: 2.5vh !important;
flex-shrink: 0 !important;
}
.eva-dep-status-text {
font-size: 2.2vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
/* --------------------------------------------------------------------------
SECTION CENTRALE : DESTINATION & GARES DESSERVIES (VIAS)
-------------------------------------------------------------------------- */
.eva-dep-col-center {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
padding: 0.6vh 1.8vw !important;
box-sizing: border-box !important;
overflow: hidden !important;
min-width: 0 !important;
}
.eva-dep-origin-row {
display: flex !important;
align-items: center !important;
gap: 0.8vw !important;
overflow: hidden !important;
white-space: nowrap !important;
margin-bottom: 0.4vh !important;
}
.eva-dep-origin-name {
font-size: 5.6vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1.1 !important;
letter-spacing: -0.02vw !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* Badge Motif de Perturbation (ex: Travaux, Panne) */
.eva-dep-perturb-badge {
display: inline-flex !important;
align-items: center !important;
gap: 0.4vw !important;
background-color: #ffa74f !important;
color: #000c2f !important;
padding: 0.4vh 0.8vw !important;
border-radius: 6px !important;
height: 3.4vh !important;
box-sizing: border-box !important;
flex-shrink: 0 !important;
}
.eva-dep-perturb-badge-icon {
width: 2.4vh !important;
height: 2.4vh !important;
flex-shrink: 0 !important;
}
.eva-dep-perturb-badge-text {
font-size: 2.2vh !important;
font-weight: 700 !important;
line-height: 1 !important;
white-space: nowrap !important;
}
/* Section Vias */
.eva-dep-vias-container {
display: flex !important;
align-items: baseline !important;
white-space: nowrap !important;
overflow: hidden !important;
width: 100% !important;
}
.eva-dep-vias-label {
font-size: 3.5vh !important;
font-weight: 700 !important;
color: #03c5ff !important; /* Bleu azur EVA Départs (#03c5ff) */
margin-right: 0.5vw !important;
flex-shrink: 0 !important;
}
.eva-dep-vias-scroll-wrapper {
flex: 1 !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
display: flex !important;
align-items: baseline !important;
}
.eva-dep-vias-inner {
display: inline-flex !important;
align-items: baseline !important;
white-space: nowrap !important;
}
.eva-dep-vias-inner--animating {
animation-name: eva-dep-vias-marquee !important;
animation-timing-function: linear !important;
animation-iteration-count: infinite !important;
padding-left: 20% !important;
}
@keyframes eva-dep-vias-marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
.eva-dep-vias-stop {
font-size: 3.5vh !important;
font-weight: 500 !important;
color: #2c3b57 !important;
line-height: 1 !important;
}
.eva-dep-vias-bullet {
color: #03c5ff !important;
font-weight: 800 !important;
margin: 0 0.5vw !important;
font-size: 2.8vh !important;
display: inline-block !important;
vertical-align: middle !important;
}
/* --------------------------------------------------------------------------
SECTION DROITE : MODE DE TRANSPORT / VOIE & OPERATEUR / NUMERO
-------------------------------------------------------------------------- */
.eva-dep-col-right {
display: flex !important;
align-items: center !important;
height: 100% !important;
flex-shrink: 0 !important;
}
/* Boîte Picto Autocar/Bus */
.eva-dep-bus-box {
background-color: #000c2f !important;
border-radius: 8px !important;
width: 7.0vw !important;
height: 80% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0.8vw !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
}
.eva-dep-bus-icon {
width: 6.0vh !important;
height: 6.0vh !important;
color: #ffffff !important;
}
/* Boîte Voie (si applicable pour train) */
.eva-dep-voie-box {
background-color: #000c2f !important;
border-radius: 8px !important;
width: 7.0vw !important;
height: 80% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
margin-right: 0.8vw !important;
color: #ffffff !important;
}
.eva-dep-voie-label {
font-size: 1.8vh !important;
font-weight: 600 !important;
text-transform: uppercase !important;
opacity: 0.8 !important;
}
.eva-dep-voie-val {
font-size: 4.5vh !important;
font-weight: 900 !important;
line-height: 1 !important;
}
/* Bloc Opérateur & Numéro Bleu Azur Départs */
.eva-dep-train-block {
background-color: #03c5ff !important; /* Bleu cyan/azur #03c5ff */
width: 10.0vw !important;
min-width: 10.0vw !important;
height: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
border-top-right-radius: 10px !important;
border-bottom-right-radius: 10px !important;
padding: 0 0.5vw !important;
box-sizing: border-box !important;
}
.eva-dep-train-op {
font-size: 2.8vh !important;
font-weight: 700 !important;
color: #000c2f !important;
line-height: 1 !important;
margin-bottom: 0.3vh !important;
text-transform: uppercase !important;
}
.eva-dep-train-num {
font-size: 4.2vh !important;
font-weight: 900 !important;
color: #000c2f !important;
line-height: 1 !important;
letter-spacing: 0.02vw !important;
}
/* ==========================================================================
BANDEAU TRAVAUX / ALERTES (Pied de page)
========================================================================== */
.eva-dep-bottom-alert {
background-color: #fca758 !important;
border-radius: 10px !important;
display: flex !important;
align-items: center !important;
padding: 0.8vh 1.8vw !important;
margin-top: 1.0vh !important;
gap: 1.4vw !important;
box-sizing: border-box !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
flex-shrink: 0 !important;
height: 10.8vh !important;
min-height: 10.8vh !important;
max-height: 10.8vh !important;
}
.eva-dep-bottom-alert-icon-wrapper {
flex-shrink: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.eva-dep-bottom-alert-icon {
width: 5.8vh !important;
height: 5.8vh !important;
color: #000c2f !important;
}
.eva-dep-bottom-alert-container {
flex: 1 !important;
overflow: hidden !important;
position: relative !important;
height: 9.0vh !important;
max-height: 9.0vh !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
}
.eva-dep-bottom-alert-content {
color: #000c2f !important;
font-size: 2.4vh !important;
font-weight: 600 !important;
line-height: 1.25 !important;
white-space: normal !important;
word-break: break-word !important;
will-change: transform, opacity !important;
}
File diff suppressed because it is too large. Load diff