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

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

No files matched your search

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