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:
1 parent
19868a451f
commit
6dfd798dd2
16 files changed
+6155
-2008
No files matched your search
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">></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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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">></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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
Reference in new issue
Block a user