V2.0.0 : VOICI LES MODIFICATIONS MAJEURS APPRTÉES DANS CETTE VERSION :
-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES (GÉNÉRAL + QUAIS) -> IMPLÉMENTATION DU DESIGN AFL : GÉNÉRAL ET QUAIS -> CHOIX DE LA SÉLECTION DES TYPES D'AFFICHEURS (GÉNÉRAL / QUAIS) UNE FOIS LE TYPE D'AFFICHEUR SÉLECTIONNÉS EN COURS DE DÉVELOPPEMENT : -> IMPLÉMENTATION DES PERTURBATION DU LANDEMAIN POUR LES HORAIRES DU LANDEMAIN AFFICHÉS AVANT MINUIT. -> IMPLÉMENTATION DES AFFICHEURS D'INFORMATIONS / INFOS TRAVAUX, etc... (AFFICHEURS AFL)
This commit is contained in:
1 parent
f0d9ed9ee7
commit
f06ebbaf48
43 files changed
+10519
-3813
No files matched your search
+13
-13
@@ -113,7 +113,7 @@
|
||||
"file": "logo-ouigo.svg",
|
||||
"path": "/img/type/logo-ouigo.svg",
|
||||
"slug": "ouigo",
|
||||
"name": "OUIGO"
|
||||
"name": "TGV Ouigo"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-classique.svg",
|
||||
@@ -263,67 +263,67 @@
|
||||
"file": "logo-ter-rhone-alpes.svg",
|
||||
"path": "/img/type/logo-ter-rhone-alpes.svg",
|
||||
"slug": "ter-rhone-alpes",
|
||||
"name": "TER Rhone Alpes"
|
||||
"name": "TER Rhone-Alpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-teralsace.svg",
|
||||
"path": "/img/type/logo-teralsace.svg",
|
||||
"slug": "teralsace",
|
||||
"name": "Teralsace"
|
||||
"name": "Ter Alsace"
|
||||
},
|
||||
{
|
||||
"file": "logo-teraquitaine.svg",
|
||||
"path": "/img/type/logo-teraquitaine.svg",
|
||||
"slug": "teraquitaine",
|
||||
"name": "Teraquitaine"
|
||||
"name": "Ter Aquitaine"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbassenormandie.svg",
|
||||
"path": "/img/type/logo-terbassenormandie.svg",
|
||||
"slug": "terbassenormandie",
|
||||
"name": "Terbassenormandie"
|
||||
"name": "Ter Basse Normandie"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbourgogne.svg",
|
||||
"path": "/img/type/logo-terbourgogne.svg",
|
||||
"slug": "terbourgogne",
|
||||
"name": "Terbourgogne"
|
||||
"name": "Ter Bourgogne"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbretagne.svg",
|
||||
"path": "/img/type/logo-terbretagne.svg",
|
||||
"slug": "terbretagne",
|
||||
"name": "Terbretagne"
|
||||
"name": "Ter Bretagne"
|
||||
},
|
||||
{
|
||||
"file": "logo-tercentre.svg",
|
||||
"path": "/img/type/logo-tercentre.svg",
|
||||
"slug": "tercentre",
|
||||
"name": "Tercentre"
|
||||
"name": "Ter Centre"
|
||||
},
|
||||
{
|
||||
"file": "logo-terlanguedocroussillon.svg",
|
||||
"path": "/img/type/logo-terlanguedocroussillon.svg",
|
||||
"slug": "terlanguedocroussillon",
|
||||
"name": "Terlanguedocroussillon"
|
||||
"name": "Ter Languedoc-Roussillon"
|
||||
},
|
||||
{
|
||||
"file": "logo-termidipyrenees.svg",
|
||||
"path": "/img/type/logo-termidipyrenees.svg",
|
||||
"slug": "termidipyrenees",
|
||||
"name": "Termidipyrenees"
|
||||
"name": "Ter Midi-Pyrenees"
|
||||
},
|
||||
{
|
||||
"file": "logo-ternordpasdecalais.svg",
|
||||
"path": "/img/type/logo-ternordpasdecalais.svg",
|
||||
"slug": "ternordpasdecalais",
|
||||
"name": "Ternordpasdecalais"
|
||||
"name": "Ter Nord-pas-de-Calais"
|
||||
},
|
||||
{
|
||||
"file": "logo-terpoitoucharentes.svg",
|
||||
"path": "/img/type/logo-terpoitoucharentes.svg",
|
||||
"slug": "terpoitoucharentes",
|
||||
"name": "Terpoitoucharentes"
|
||||
"name": "Ter Poitou Charentes"
|
||||
},
|
||||
{
|
||||
"file": "logo-tgv.svg",
|
||||
@@ -359,7 +359,7 @@
|
||||
"file": "track-car.svg",
|
||||
"path": "/img/type/track-car.svg",
|
||||
"slug": "track-car",
|
||||
"name": "Track Car"
|
||||
"name": "Car TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-tramtrain.svg",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY;
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function run() {
|
||||
const { data: gares, error: err1 } = await supabase.from('gares').select('*').ilike('nom', '%Ouges%').limit(1);
|
||||
console.log("Gares:", gares);
|
||||
if (err1) console.error(err1);
|
||||
|
||||
const { data: horaires, error: err2 } = await supabase.from('horaires').select('*').limit(5);
|
||||
// find one that stops at Ouges
|
||||
const { data: horairesOuges, error: err3 } = await supabase.from('horaires').select('*').ilike('gares_desservies', '%Ouges%').limit(2);
|
||||
console.log("Horaires Ouges:", JSON.stringify(horairesOuges, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,11 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%').limit(5);
|
||||
console.log("Horaires Ouges:", JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,13 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').limit(10);
|
||||
// find any train with Ouges
|
||||
const filtered = data.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
console.log("Horaires Ouges filtered:", JSON.stringify(filtered, null, 2));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,12 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('*').limit(1);
|
||||
console.log("Horaires schema:", Object.keys(data[0] || {}));
|
||||
if (error) console.error("Error:", error);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,19 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
// try to fetch all and filter in JS
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').limit(200);
|
||||
const filtered = allData.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
console.log("Horaires Ouges (JS filter):", JSON.stringify(filtered.slice(0, 2), null, 2));
|
||||
} else {
|
||||
console.log("Horaires Ouges:", JSON.stringify(data.slice(0, 2), null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+24
@@ -10,6 +10,11 @@ import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees
|
||||
import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais';
|
||||
import sessionMemory from './lib/sessionMemory';
|
||||
import ClassiquesArriveesQuais from "./pages/afficheurs/classiques/quais/ClassiquesArriveesQuais";
|
||||
import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
|
||||
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
|
||||
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
|
||||
import AFLArriveesQuais from './pages/afficheurs/afl/AFLArriveesQuais';
|
||||
import SelectAfficheur from './pages/SelectAfficheur';
|
||||
applyPolyfills().then(() => defineCustomElements(window));
|
||||
|
||||
// Petit routeur client sans dépendances externes
|
||||
@@ -130,6 +135,18 @@ function App() {
|
||||
if (route.startsWith('/afficheurs/departs/classiques')) {
|
||||
return <ClassiquesDeparts />;
|
||||
}
|
||||
if (route.startsWith('/afficheurs/departs/afl/quais')) {
|
||||
return <AFLDepartsQuais />;
|
||||
}
|
||||
if (route.startsWith('/afficheurs/departs/afl')) {
|
||||
return <AFLDeparts />;
|
||||
}
|
||||
if (route.startsWith('/afficheurs/arrivees/afl/quais')) {
|
||||
return <AFLArriveesQuais />;
|
||||
}
|
||||
if (route.startsWith('/afficheurs/arrivees/afl')) {
|
||||
return <AFLArrivees />;
|
||||
}
|
||||
if (route.startsWith('/afficheurs/arrivees/classiques')) {
|
||||
return <ClassiquesArrivees />;
|
||||
}
|
||||
@@ -139,6 +156,13 @@ function App() {
|
||||
content = <Departs onBack={() => navigate('/')} navigate={navigate} />;
|
||||
} else if (route === '/arrivees') {
|
||||
content = <Arrivees onBack={() => navigate('/')} navigate={navigate} />;
|
||||
} else if (route === '/select-afficheur') {
|
||||
content = <SelectAfficheur onBack={() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const gare = params.get('gare') || '';
|
||||
const mode = params.get('mode') || 'departs';
|
||||
navigate(`/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
|
||||
}} navigate={navigate} />;
|
||||
} else if (route.startsWith('/afficheurs')) {
|
||||
content = <Displays onBack={() => navigate('/')} navigate={navigate} />;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import useTypeLogos from '../hooks/useTypeLogos';
|
||||
|
||||
function getTypeLogo(type, map) {
|
||||
if (!type) return map.default;
|
||||
const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-').replace(/-$/, '');
|
||||
if (map[norm]) return map[norm];
|
||||
|
||||
// Also try to match the first word
|
||||
const firstWord = norm.split('-')[0];
|
||||
if (firstWord && map[firstWord]) return map[firstWord];
|
||||
|
||||
const keys = Object.keys(map).filter(k => k !== 'default');
|
||||
for (const k of keys) {
|
||||
if (k === norm || k.includes(norm) || norm.includes(k)) return map[k];
|
||||
}
|
||||
return map.default;
|
||||
}
|
||||
|
||||
export default function Logo({ op, classNameBox = '', classNameImg = '', classNameText = '' }) {
|
||||
const typeLogos = useTypeLogos();
|
||||
const [hasImg, setHasImg] = useState(true);
|
||||
|
||||
const src = getTypeLogo(op, typeLogos);
|
||||
|
||||
useEffect(() => {
|
||||
setHasImg(true); // Reset when op or typeLogos changes
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<div className={classNameBox} aria-hidden>
|
||||
{hasImg && src ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={op}
|
||||
className={classNameImg}
|
||||
onError={() => setHasImg(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className={classNameText} aria-hidden>{(op || '').split(' ')[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@keyframes marquee-scroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
.marquee-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import './MarqueeText.css';
|
||||
|
||||
const MarqueeText = ({ text, className }) => {
|
||||
const containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (containerRef.current && textRef.current) {
|
||||
setIsOverflowing(textRef.current.scrollWidth > containerRef.current.clientWidth);
|
||||
}
|
||||
};
|
||||
checkOverflow();
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
return () => window.removeEventListener('resize', checkOverflow);
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<div className={`marquee-container ${className || ''}`} ref={containerRef} style={{ overflow: 'hidden', whiteSpace: 'nowrap', width: '100%', flex: '1 1 0' }}>
|
||||
<div
|
||||
ref={textRef}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
animation: isOverflowing ? 'marquee-scroll 10s linear infinite' : 'none',
|
||||
paddingLeft: isOverflowing ? '100%' : '0'
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarqueeText;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function useTypeLogos(isQuai = false) {
|
||||
const [map, setMap] = useState({ default: '/type/sncf-voyageurs-logo.png' });
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const load = async () => {
|
||||
const candidates = isQuai ? ['/type/quais/data-color.json', '/type/data.json'] : ['/type/data.json'];
|
||||
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) continue;
|
||||
const data = await res.json();
|
||||
if (!mounted) return;
|
||||
|
||||
const logos = {};
|
||||
if (Array.isArray(data.logos)) {
|
||||
data.logos.forEach(item => {
|
||||
if (!item.name || !item.path) return;
|
||||
|
||||
const key = String(item.name).toUpperCase().replace(/[^A-Z0-9]+/g, '-').replace(/-$/, '');
|
||||
|
||||
let path = item.path.replace('/img', '');
|
||||
|
||||
logos[key] = path;
|
||||
|
||||
if (item.slug) {
|
||||
const slugKey = item.slug.toUpperCase().replace(/[^A-Z0-9]+/g, '-').replace(/-$/, '');
|
||||
if (!logos[slugKey]) {
|
||||
logos[slugKey] = path;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logos.default = logos['SNCF'] || logos['TER'] || '/type/sncf-voyageurs-logo.png';
|
||||
|
||||
setMap(logos);
|
||||
return; // Stop after first successful fetch
|
||||
} catch (e) {
|
||||
console.error(`Failed to load logos data from ${url}`, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
return () => { mounted = false; };
|
||||
}, [isQuai]);
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -60,14 +60,9 @@ export default function Displays({ onBack, navigate }) {
|
||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16 }}>
|
||||
{types.map((t) => (
|
||||
<div key={t.id} role="button" tabIndex={0} style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }} onClick={() => {
|
||||
// Si un navigate est fourni par le routeur simple, on l'utilise
|
||||
if (typeof navigate === 'function') {
|
||||
// construire l'URL finale au format strict demandé :
|
||||
// /afficheurs/{mode}/{type}?gare=<Nom-de-la-gare>
|
||||
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
|
||||
const gareQuery = kebab ? `?gare=${encodeURIComponent(kebab)}` : '';
|
||||
// Construire l'URL au format demandé : /afficheurs/{mode}/{type}?gare=...
|
||||
navigate(`/afficheurs/${mode}/${t.id}${gareQuery}`);
|
||||
navigate(`/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
|
||||
return;
|
||||
}
|
||||
alert(`${t.label} pour ${gare || '—'}`);
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import supabase from '../lib/supabase';
|
||||
import WcsButton from '../components/WcsButton';
|
||||
|
||||
export default function SelectAfficheur({ onBack, navigate }) {
|
||||
const params = new URLSearchParams(window.location.search || '');
|
||||
const gareParam = params.get('gare') || '';
|
||||
const modeParam = params.get('mode') || 'departs';
|
||||
const typeParam = params.get('type') || 'afl';
|
||||
|
||||
const [platforms, setPlatforms] = useState([]);
|
||||
const [selectedPlatform, setSelectedPlatform] = useState('');
|
||||
const [manualPlatform, setManualPlatform] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Decode and format station name for display
|
||||
const decodedGare = gareParam
|
||||
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
async function loadPlatforms() {
|
||||
if (!supabase) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase.from('horaires').select('stops, quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom');
|
||||
if (error) throw error;
|
||||
if (!mounted) return;
|
||||
|
||||
const platformSet = new Set();
|
||||
const normGare = decodedGare.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
|
||||
(data || []).forEach(h => {
|
||||
let stopsList = [];
|
||||
if (Array.isArray(h.stops)) {
|
||||
stopsList = h.stops;
|
||||
} else if (typeof h.stops === 'string') {
|
||||
try { stopsList = JSON.parse(h.stops); } catch (e) {}
|
||||
}
|
||||
if (!Array.isArray(stopsList)) stopsList = [];
|
||||
|
||||
let foundStop = stopsList.find(s => {
|
||||
const name = (s.gare_nom || s.nom || s.name || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return name === normGare || name.includes(normGare) || normGare.includes(name);
|
||||
});
|
||||
|
||||
if (foundStop) {
|
||||
const platform = (foundStop.quai_reel || foundStop.quai || foundStop.platform || foundStop.quay || '').toString().trim();
|
||||
if (platform) platformSet.add(platform);
|
||||
} else {
|
||||
const origin = (h.gare_depart_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
const dest = (h.gare_arrivee_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
if (origin === normGare || origin.includes(normGare) || normGare.includes(origin) ||
|
||||
dest === normGare || dest.includes(normGare) || normGare.includes(dest)) {
|
||||
const platform = (h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (platform) platformSet.add(platform);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sortedPlatforms = Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
setPlatforms(sortedPlatforms);
|
||||
if (sortedPlatforms.length > 0) {
|
||||
setSelectedPlatform(sortedPlatforms[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching platforms:', err);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadPlatforms();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [gareParam, decodedGare]);
|
||||
|
||||
const handleGeneral = () => {
|
||||
if (typeof navigate === 'function') {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlatform = () => {
|
||||
const quai = manualPlatform.trim() || selectedPlatform;
|
||||
if (!quai) {
|
||||
alert("Veuillez sélectionner ou saisir une voie.");
|
||||
return;
|
||||
}
|
||||
if (typeof navigate === 'function') {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '650px',
|
||||
margin: '40px auto',
|
||||
padding: '32px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
|
||||
}}>
|
||||
<h2 style={{
|
||||
marginTop: 0,
|
||||
marginBottom: '8px',
|
||||
color: '#002257',
|
||||
fontSize: '28px',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
Sélection de l'Afficheur
|
||||
</h2>
|
||||
<p style={{
|
||||
margin: '0 0 24px 0',
|
||||
color: '#555555',
|
||||
fontSize: '16px',
|
||||
lineHeight: 1.5
|
||||
}}>
|
||||
Pour la gare de <strong>{decodedGare}</strong> en mode <strong>{modeParam === 'arrivees' ? 'Arrivées' : 'Départs'}</strong>.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginBottom: '32px' }}>
|
||||
{/* General Afficheur Option */}
|
||||
<div style={{
|
||||
border: '2px solid #e2ebf6',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
background: '#fafbfc'
|
||||
}}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur Général</h3>
|
||||
<p style={{ margin: '0 0 20px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
||||
Affiche la liste complète de tous les trains programmés pour cette gare.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGeneral}
|
||||
style={{
|
||||
backgroundColor: '#00439e',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 20px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
>
|
||||
Accéder à l'Afficheur
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Platform-specific Afficheur Option */}
|
||||
<div style={{
|
||||
border: '2px solid #e2ebf6',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
background: '#fafbfc'
|
||||
}}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur par Quai</h3>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
||||
Affiche le prochain train et sa desserte spécifiquement pour une voie/quai.
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ fontSize: '14px', color: '#888', marginBottom: '16px' }}>Chargement des voies...</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{platforms.length > 0 ? (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 600, color: '#333', marginBottom: '4px' }}>
|
||||
Sélectionner une voie existante :
|
||||
</label>
|
||||
<select
|
||||
value={selectedPlatform}
|
||||
onChange={(e) => {
|
||||
setSelectedPlatform(e.target.value);
|
||||
setManualPlatform(''); // Reset manual entry on selection
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ccc',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{platforms.map(p => (
|
||||
<option key={p} value={p}>Voie {p}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '13px', color: '#ff6b6b', marginBottom: '12px' }}>
|
||||
Aucune voie détectée automatiquement pour cette gare.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 600, color: '#333', marginBottom: '4px' }}>
|
||||
Saisie libre d'une voie :
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ex: A, 2 bis, 4..."
|
||||
value={manualPlatform}
|
||||
onChange={(e) => setManualPlatform(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ccc',
|
||||
fontSize: '14px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePlatform}
|
||||
style={{
|
||||
backgroundColor: '#008b47',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 20px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
>
|
||||
Accéder au Quai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderTop: '1px solid #eeeeee',
|
||||
paddingTop: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start'
|
||||
}}>
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={() => {
|
||||
if (typeof onBack === 'function') {
|
||||
onBack();
|
||||
} else if (typeof navigate === 'function') {
|
||||
navigate(`/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
|
||||
}
|
||||
}}
|
||||
label="Retour aux types d'afficheurs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.afl-arrivees__container {
|
||||
font-family: 'Achemine', Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1e4a30; /* Vivid dark blue background */
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.afl-arrivees__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #216b3e; /* Bright blue header */
|
||||
padding: 0 2vw;
|
||||
height: 10vh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.afl-arrivees__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5vw;
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.afl-arrivees__title {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.afl-arrivees__header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1vw;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.afl-arrivees__page-dot {
|
||||
width: 4vh;
|
||||
height: 4vh;
|
||||
border-radius: 50%;
|
||||
background-color: #1e4a30; /* Dark blue for inactive */
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.afl-arrivees__page-dot--active {
|
||||
background-color: #4caf50; /* Cyan for active */
|
||||
}
|
||||
|
||||
.afl-arrivees__header-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.afl-arrivees__header-right .aff-classiques-departs__clock-box {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.afl-arrivees__header-right .aff-classiques-departs__clock-main {
|
||||
font-size: 4.5vh;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.afl-arrivees__header-right .aff-classiques-departs__clock-seconds {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.afl-arrivees__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.afl-arrivees__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 2.5vh 2vw 0 2vw;
|
||||
position: relative;
|
||||
height: 17.5vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-arrivees__col-time-status {
|
||||
width: 16vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.afl-arrivees__time {
|
||||
font-size: 7vh;
|
||||
font-weight: 700;
|
||||
color: #fce205; /* Bright yellow */
|
||||
line-height: 1;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.afl-arrivees__time--suppressed {
|
||||
color: #ff4444;
|
||||
font-size: 4vh;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.afl-arrivees__status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #216b3e; /* Blue background matching header */
|
||||
border-radius: 3vh;
|
||||
padding: 0.6vh 0.8vw;
|
||||
margin-top: 1vh;
|
||||
font-size: 2.5vh;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.afl-arrivees__status-pill svg {
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
|
||||
.afl-arrivees__status-pill--delayed {
|
||||
background-color: #ff9e43;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-arrivees__status-pill--suppressed {
|
||||
background-color: #ff4c5b;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-arrivees__col-train-info {
|
||||
width: 12vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.5vh;
|
||||
align-items: flex-start; /* Left align type and number */
|
||||
}
|
||||
|
||||
.afl-arrivees__op-name {
|
||||
font-size: 3vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.afl-arrivees__op-number {
|
||||
font-size: 2.5vh;
|
||||
font-weight: 400;
|
||||
color: #a4d2b1;
|
||||
margin-top: 0.5vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__col-destination {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 2vw;
|
||||
padding-right: 2vw;
|
||||
padding-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.afl-arrivees__dest-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__destination-name {
|
||||
font-size: 6vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.afl-arrivees__vias-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.afl-arrivees__vias-static {
|
||||
font-size: 3.5vh;
|
||||
font-weight: 400;
|
||||
color: #a4d2b1;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.afl-arrivees__vias-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.afl-arrivees__vias {
|
||||
font-size: 3.5vh;
|
||||
font-weight: 400;
|
||||
color: #a4d2b1;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
animation: afl-marquee 6.6s linear infinite;
|
||||
padding-left: 100%;
|
||||
}
|
||||
|
||||
@keyframes afl-marquee {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(-100%, 0); }
|
||||
}
|
||||
|
||||
.afl-arrivees__col-platform {
|
||||
width: 12vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.afl-arrivees__platform-box {
|
||||
background-color: white;
|
||||
border-radius: 0.8vh;
|
||||
width: 10vw;
|
||||
height: 12.5vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0.5vh 1vh rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.afl-arrivees__platform-label {
|
||||
color: #1e4a30; /* Matches background */
|
||||
font-size: 2.8vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.afl-arrivees__platform-value {
|
||||
color: #1e4a30; /* Matches background */
|
||||
font-size: 6.8vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.afl-arrivees__separator-wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.afl-arrivees__separator {
|
||||
position: absolute;
|
||||
height: 0.4vh;
|
||||
background-color: #16552a;
|
||||
width: 96vw;
|
||||
border-radius: 0.2vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__train-illustration {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: #1e4a30; /* matches background */
|
||||
padding: 0 1vw;
|
||||
}
|
||||
|
||||
.afl-arrivees__train-illustration svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body:has(.afl-arrivees__container) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.afl-arrivees__container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-arrivees__separator-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.afl-arrivees__separator {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
background-color: #16552a;
|
||||
width: calc(100% - 48px);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.afl-arrivees__train-illustration {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: #01347d; /* match row background so it overlays the line */
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.afl-arrivees__row--compact {
|
||||
height: 10.5vh;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.afl-arrivees__row--compact .afl-arrivees__platform-box {
|
||||
height: 8vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__row--compact .afl-arrivees__platform-label {
|
||||
font-size: 1.8vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__row--compact .afl-arrivees__platform-value {
|
||||
font-size: 4vh;
|
||||
}
|
||||
|
||||
.afl-arrivees__row--compact .afl-arrivees__col-time-status,
|
||||
.afl-arrivees__row--compact .afl-arrivees__col-train-info,
|
||||
.afl-arrivees__row--compact .afl-arrivees__col-destination,
|
||||
.afl-arrivees__row--compact .afl-arrivees__col-platform {
|
||||
padding-top: 0;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,334 @@
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__container {
|
||||
--bg-color: #1e4a30; /* Deep dark green background */
|
||||
--header-bg: #216b3e; /* Lighter vibrant green header */
|
||||
--accent-green: #4caf50; /* Green timeline/active color */
|
||||
--accent-yellow: #fce205; /* Bright yellow for arrival time */
|
||||
--text-grey: #a4d2b1; /* Light green-grey for train info */
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
color: white;
|
||||
font-family: 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* --- HEADER --- */
|
||||
.afl-quais-arrivees__header {
|
||||
height: 12vh;
|
||||
background-color: var(--header-bg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 3vw;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2vw;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__header-title {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__header-clock {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* --- MAIN CONTENT --- */
|
||||
.afl-quais-arrivees__main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- LEFT PANE (GLUED TO LEFT/TOP, ROUNDED BOTTOM RIGHT) --- */
|
||||
.afl-quais-arrivees__left-pane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35vw;
|
||||
height: 38vh;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 0 0 24px 0; /* Only bottom-right is rounded */
|
||||
padding: 4vh 3vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__time-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__time {
|
||||
font-size: 7.5vh;
|
||||
font-weight: 800;
|
||||
color: var(--accent-yellow);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #216b3e; /* Green background matching header */
|
||||
border-radius: 3vh;
|
||||
padding: 0.6vh 0.8vw;
|
||||
font-size: 2.2vh;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__status-pill svg {
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__status-pill--delayed {
|
||||
background-color: #ff9e43;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__status-pill--suppressed {
|
||||
background-color: #ff4c5b;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__destination {
|
||||
font-size: 5.5vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: #ffffff;
|
||||
margin-bottom: 1.5vh;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__train-info {
|
||||
font-size: 3vh;
|
||||
color: var(--text-grey);
|
||||
display: flex;
|
||||
gap: 0.8vw;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__train-type {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__train-number {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- RIGHT PANE (TIMELINE) --- */
|
||||
.afl-quais-arrivees__right-pane {
|
||||
position: absolute;
|
||||
top: 4vh;
|
||||
left: 38vw;
|
||||
width: 58vw;
|
||||
bottom: 12vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* Center stops list vertically */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stops-window {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stops-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 6.2vh;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Timeline Indicator (Line segment + Dot) */
|
||||
.afl-quais-arrivees__stop-indicator {
|
||||
position: relative;
|
||||
width: 4vw;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stop-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.6vh;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Adjust line ends for first and last stops */
|
||||
.afl-quais-arrivees__stop-item.is-first .afl-quais-arrivees__stop-line {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stop-item.is-last .afl-quais-arrivees__stop-line {
|
||||
bottom: 50%;
|
||||
}
|
||||
|
||||
/* Standard dot */
|
||||
.afl-quais-arrivees__stop-dot {
|
||||
width: 1.4vh;
|
||||
height: 1.4vh;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Terminus dot */
|
||||
.afl-quais-arrivees__stop-dot.is-terminus {
|
||||
width: 2.6vh;
|
||||
height: 2.6vh;
|
||||
background-color: var(--bg-color); /* Hollow effect */
|
||||
border: 0.6vh solid var(--accent-green);
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.8);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Stop Names */
|
||||
.afl-quais-arrivees__stop-name {
|
||||
font-size: 3.8vh;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
padding-left: 1vw;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__stop-name.is-terminus {
|
||||
font-size: 4.5vh;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
/* --- BOTTOM TRAIN ILLUSTRATION --- */
|
||||
.afl-quais-arrivees__bottom-container {
|
||||
position: absolute;
|
||||
bottom: 3.5vh;
|
||||
left: 38vw;
|
||||
width: 42vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__train-illustration {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* --- VOIE BOX (PLATFORM BOX) --- */
|
||||
.afl-quais-arrivees__platform-box {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
right: 2vw;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
width: 6.5vw;
|
||||
height: 13vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.35);
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__platform-label {
|
||||
font-size: 2.2vh;
|
||||
color: var(--bg-color);
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
|
||||
.afl-quais-arrivees__platform-value {
|
||||
font-size: 7vh;
|
||||
color: var(--bg-color);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* --- NO DEPARTURE / LOADING STATE --- */
|
||||
.afl-quais-arrivees__no-departure {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 88vh;
|
||||
font-size: 6vh;
|
||||
font-weight: 600;
|
||||
color: #a4d2b1;
|
||||
}
|
||||
@@ -0,0 +1,866 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import './AFLArriveesQuais.css';
|
||||
|
||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
const tryDate = new Date(v);
|
||||
if (!isNaN(tryDate.getTime())) {
|
||||
const hh = String(tryDate.getHours()).padStart(2, '0');
|
||||
const mm = String(tryDate.getMinutes()).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
const parts = String(v).split(':');
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const AFLArriveesQuais = () => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [nextArrival, setNextArrival] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Parse URL manually since we are not in a React Router context
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const gareParam = searchParams.get('gare') || '';
|
||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
|
||||
// Real-time clock update
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// --- Helpers pour perturbations, travaux, etc ---
|
||||
const normalizeName = useCallback((s) => {
|
||||
if (!s) return '';
|
||||
try {
|
||||
const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
|
||||
} catch (e) {
|
||||
return String(s).toLowerCase().trim();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const safeParseArrets = useCallback((p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
if (Array.isArray(parsed.arrets)) return parsed.arrets;
|
||||
return parsed;
|
||||
}
|
||||
return [];
|
||||
} catch (e) { return []; }
|
||||
}, []);
|
||||
|
||||
const getStops = useCallback((h) => {
|
||||
if (!h) return [];
|
||||
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
|
||||
if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies;
|
||||
if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies;
|
||||
const candidates = ['stops', 'gares_desservies', 'garesDesservies', 'gares_desservies_json'];
|
||||
for (const key of candidates) {
|
||||
const raw = h[key];
|
||||
if (!raw) continue;
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (typeof raw === 'string') {
|
||||
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const parsePerturbArretsObject = useCallback((p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return { arrets: [], jours: null };
|
||||
if (typeof raw === 'object') {
|
||||
if (Array.isArray(raw.arrets)) return { arrets: raw.arrets, jours: raw.jours_perturbation || raw.jours || null };
|
||||
if (Array.isArray(raw)) return { arrets: raw, jours: null };
|
||||
return { arrets: [], jours: null };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return { arrets: Array.isArray(parsed.arrets) ? parsed.arrets : (Array.isArray(parsed) ? parsed : []), jours: parsed.jours_perturbation || parsed.jours || null };
|
||||
}
|
||||
return { arrets: [], jours: null };
|
||||
} catch (e) {
|
||||
return { arrets: [], jours: null };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPerturbationJours = useCallback((p) => {
|
||||
if (!p) return null;
|
||||
const candidate = p.jours_perturbation || p.jours || null;
|
||||
const result = new Set();
|
||||
const pushDate = (d) => {
|
||||
if (!d) return;
|
||||
try {
|
||||
const dd = new Date(d);
|
||||
if (!isNaN(dd.getTime())) result.add(dd.toISOString().slice(0, 10));
|
||||
} catch (e) {}
|
||||
};
|
||||
if (candidate) {
|
||||
if (Array.isArray(candidate)) {
|
||||
candidate.forEach(item => pushDate(item.date || item));
|
||||
} else if (typeof candidate === 'string') {
|
||||
try { const parsed = JSON.parse(candidate); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); }
|
||||
catch (e) { pushDate(candidate); }
|
||||
} else if (typeof candidate === 'object') {
|
||||
if (Array.isArray(candidate)) candidate.forEach(it => pushDate(it.date || it));
|
||||
else pushDate(candidate.date || null);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const parsedObj = parsePerturbArretsObject(p);
|
||||
const jp = parsedObj.jours;
|
||||
if (jp) {
|
||||
if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it));
|
||||
else if (typeof jp === 'string') {
|
||||
try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); }
|
||||
} else if (typeof jp === 'object') pushDate(jp.date || null);
|
||||
}
|
||||
} catch (e) {}
|
||||
return result.size ? result : null;
|
||||
}, [parsePerturbArretsObject]);
|
||||
|
||||
const parseDelayMinutesFromArret = useCallback((arret) => {
|
||||
if (!arret) return null;
|
||||
const keys = ['retard_minutes', 'retard', 'delay_minutes', 'delay', 'retard_min'];
|
||||
for (const k of keys) {
|
||||
if (typeof arret[k] !== 'undefined' && arret[k] !== null && arret[k] !== '') {
|
||||
const n = Number(arret[k]);
|
||||
if (!isNaN(n)) return Math.round(n);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const parseTimeHM = useCallback((s) => {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'number') return s;
|
||||
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
|
||||
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const buildPerturbMap = useCallback((list, allHorairesList) => {
|
||||
const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] };
|
||||
(list || []).forEach(p => {
|
||||
const horaireId = p.horaire_id || p.horaire || p.horaireId || p.horaireID;
|
||||
if (horaireId != null) { const k = String(horaireId); idx.byHoraire[k] = idx.byHoraire[k] || []; idx.byHoraire[k].push(p); }
|
||||
const number = p.number || p.train_number || p.train || p.numero || p.numero_train;
|
||||
if (number != null) { const kn = String(number); idx.byNumber[kn] = idx.byNumber[kn] || []; idx.byNumber[kn].push(p); }
|
||||
const arrets = safeParseArrets(p);
|
||||
if (Array.isArray(arrets) && arrets.length) {
|
||||
arrets.forEach(a => {
|
||||
const stopId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code_uic || a.gare_uic || a.code;
|
||||
const stopName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
if (stopId != null) { const ks = String(stopId); idx.byStopId[ks] = idx.byStopId[ks] || []; idx.byStopId[ks].push({ perturbation: p, arret: a }); }
|
||||
if (stopName) { const key = normalizeName(stopName); idx.byStopName[key] = idx.byStopName[key] || []; idx.byStopName[key].push({ perturbation: p, arret: a }); }
|
||||
});
|
||||
}
|
||||
if (horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length)) { idx.global.push(p); }
|
||||
});
|
||||
if (Array.isArray(allHorairesList) && allHorairesList.length) {
|
||||
try {
|
||||
const byH = Object.keys(idx.byHoraire || {});
|
||||
if (byH && byH.length) {
|
||||
for (const hid of byH) {
|
||||
const h = allHorairesList.find(x => String(x.id) === String(hid));
|
||||
if (h) {
|
||||
const num = h.numero_train || h.number || h.numero || null;
|
||||
if (num != null) {
|
||||
const numKey = String(num);
|
||||
idx.byNumber[numKey] = idx.byNumber[numKey] || [];
|
||||
(idx.byHoraire[hid] || []).forEach(p => idx.byNumber[numKey].push(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return idx;
|
||||
}, [safeParseArrets, normalizeName]);
|
||||
|
||||
|
||||
const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
|
||||
const map = idx || buildPerturbMap();
|
||||
const allPerturbs = [];
|
||||
const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train);
|
||||
const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number);
|
||||
|
||||
if (map) {
|
||||
const addedIds = new Set();
|
||||
const addP = (p) => {
|
||||
if (p && !addedIds.has(p.id)) {
|
||||
addedIds.add(p.id);
|
||||
allPerturbs.push(p);
|
||||
}
|
||||
};
|
||||
if (hId != null && map.byHoraire && map.byHoraire[String(hId)]) {
|
||||
map.byHoraire[String(hId)].forEach(addP);
|
||||
}
|
||||
if (number != null && map.byNumber && map.byNumber[String(number)]) {
|
||||
map.byNumber[String(number)].forEach(addP);
|
||||
}
|
||||
if (stationId != null && map.byStopId && map.byStopId[String(stationId)]) {
|
||||
map.byStopId[String(stationId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (stationName && map.byStopName) {
|
||||
const key = normalizeName(stationName);
|
||||
if (map.byStopName[key]) map.byStopName[key].forEach(x => addP(x.perturbation));
|
||||
for (const existingKey of Object.keys(map.byStopName)) {
|
||||
if (existingKey.includes(key) || key.includes(existingKey)) {
|
||||
map.byStopName[existingKey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
if (Array.isArray(trainStops)) {
|
||||
trainStops.forEach(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && map.byStopId && map.byStopId[String(sId)]) {
|
||||
map.byStopId[String(sId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (sName && map.byStopName) {
|
||||
const skey = normalizeName(sName);
|
||||
if (map.byStopName[skey]) map.byStopName[skey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (map.global) {
|
||||
map.global.forEach(addP);
|
||||
}
|
||||
}
|
||||
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); };
|
||||
let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
|
||||
if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
|
||||
|
||||
let maxDelay = null;
|
||||
let isSuppressed = false;
|
||||
|
||||
for (const p of allPerturbs) {
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== horaireYmd) continue;
|
||||
if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && horaireYmd && !joursSet.has(horaireYmd)) continue;
|
||||
} catch (e) {}
|
||||
|
||||
const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
|
||||
const numberRef = p.number || p.train_number || p.numero || p.numero_train;
|
||||
const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
|
||||
const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
|
||||
|
||||
const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
|
||||
const arrets = parsedObj.arrets || [];
|
||||
|
||||
const trainStops = getStops(horaire);
|
||||
let trainStopMatch = false;
|
||||
|
||||
const matchesTarget = (a) => {
|
||||
if (!stationName && stationId == null) return false;
|
||||
const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
if (stationId != null && String(aId) === String(stationId)) return true;
|
||||
if (stationName && aName) {
|
||||
const na = normalizeName(aName);
|
||||
const ns = normalizeName(stationName);
|
||||
return na === ns || na.includes(ns) || ns.includes(na);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const a of arrets) {
|
||||
const aId = a.gare_id || a.gareId || a.id || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
const na = normalizeName(aName || '');
|
||||
|
||||
const isTarget = matchesTarget(a);
|
||||
let isTrainStop = false;
|
||||
if (trainStops && Array.isArray(trainStops)) {
|
||||
isTrainStop = trainStops.some(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && aId != null && String(sId) === String(aId)) return true;
|
||||
if (sName && aName) {
|
||||
const ns = normalizeName(sName);
|
||||
return ns === na || ns.includes(na) || na.includes(ns);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const originName = normalizeName(horaire.gare_depart_nom || horaire.gare_depart || '');
|
||||
const destName = normalizeName(horaire.gare_arrivee_nom || horaire.gare_arrivee || '');
|
||||
if (originName === na || destName === na) isTrainStop = true;
|
||||
|
||||
if (isTrainStop) trainStopMatch = true;
|
||||
|
||||
if (isTarget) {
|
||||
const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = (arrets.length === 0) && !ref && !numberRef;
|
||||
const applies = appliesByRef || appliesByNumber || trainStopMatch || isGlobal;
|
||||
|
||||
if (applies) {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
|
||||
for (const a of arrets) {
|
||||
const tType = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (tType.includes('retard') || tType.includes('delay') || /retard/.test(tType)) {
|
||||
const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
const applyPerturbationsToHoraires = useCallback((list, perturbList, displayStationId, displayStationName, serviceDate, map) => {
|
||||
const targetStationId = displayStationId || null;
|
||||
const targetStationName = displayStationName || null;
|
||||
|
||||
return (list || []).map(h => {
|
||||
const horaire = { ...h };
|
||||
const stops = getStops(horaire);
|
||||
const stopsStatus = [];
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const s = stops[i] || {};
|
||||
const stationId = s.gare_id || s.gareId || s.id || s.station_id || s.stationId || s.uic || s.code;
|
||||
const stationName = s.gare_nom || s.nom || s.name || s.station_name || s.station_nom;
|
||||
const st = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
|
||||
stopsStatus.push({ stationId, stationName, status: st });
|
||||
}
|
||||
|
||||
const displayStatus = computeStatusForHoraireAtStop(horaire, targetStationId, targetStationName, map, serviceDate);
|
||||
let displayTime = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||
horaire._stopsStatus = stopsStatus;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
return horaire;
|
||||
});
|
||||
}, [computeStatusForHoraireAtStop, getStops]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
|
||||
supabase.from('horaires').select('*'),
|
||||
supabase.from('travaux_substitutions').select('*'),
|
||||
supabase.from('perturbations').select('*'),
|
||||
supabase.from('travaux').select('*')
|
||||
]);
|
||||
|
||||
let parsedHoraires = (horairesData || []).map(h => {
|
||||
try {
|
||||
if (h && typeof h.stops === 'string') {
|
||||
const parsed = JSON.parse(h.stops);
|
||||
if (Array.isArray(parsed)) return { ...h, stops: parsed };
|
||||
}
|
||||
} catch (e) {}
|
||||
return h || {};
|
||||
});
|
||||
|
||||
const substitutions = subData || [];
|
||||
const travauxList = travauxData || [];
|
||||
|
||||
// Link substitutions
|
||||
parsedHoraires = parsedHoraires.map(h => {
|
||||
const subs = substitutions.filter(s => String(s.horaire_id) === String(h.id));
|
||||
if (subs.length > 0) {
|
||||
return { ...h, _substitutions: subs };
|
||||
}
|
||||
return h;
|
||||
});
|
||||
|
||||
// Normaliser date
|
||||
const dateKeys = ['date_depart', 'date', 'jour', 'service_date', 'date_service', 'date_dep'];
|
||||
const normalizeDate = (o) => {
|
||||
for (const k of dateKeys) {
|
||||
if (!o) continue;
|
||||
const v = o[k];
|
||||
if (!v) continue;
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const withDates = parsedHoraires.map(h => ({ ...h, _date: normalizeDate(h) }));
|
||||
|
||||
// Time parsing & enrichment
|
||||
const now = new Date();
|
||||
const todayHM = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const enrich = withDates.map(h => {
|
||||
let raw = h.heure_arrivee || h.time || h.heure || '';
|
||||
const stopsListForH = getStops(h);
|
||||
if (gareDisplay && Array.isArray(stopsListForH)) {
|
||||
const stop = stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
|
||||
}
|
||||
const depHM = parseTimeHM(raw);
|
||||
const displayTime = formatTimeNoSeconds(raw);
|
||||
return { ...h, _rawTime: raw, _depHM: depHM, _displayTime: displayTime };
|
||||
});
|
||||
|
||||
// Jours de circulation
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const localDateStr = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
const today = new Date();
|
||||
const todayStr = localDateStr(today);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
const tomorrowStr = localDateStr(tomorrow);
|
||||
|
||||
const weekdayIndex = (d) => d.getDay();
|
||||
const weekdayNameForColumn = (d) => {
|
||||
const i = weekdayIndex(d);
|
||||
switch (i) {
|
||||
case 1: return 'circule_lundi';
|
||||
case 2: return 'circule_mardi';
|
||||
case 3: return 'circule_mercredi';
|
||||
case 4: return 'circule_jeudi';
|
||||
case 5: return 'circule_vendredi';
|
||||
case 6: return 'circule_samedi';
|
||||
case 0: return 'circule_dimanche';
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isPublicHoliday = (d) => {
|
||||
const y = d.getFullYear();
|
||||
const m = d.getMonth() + 1;
|
||||
const day = d.getDate();
|
||||
const s = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const holidays = [
|
||||
`${y}-01-01`, `${y}-05-01`, `${y}-05-08`, `${y}-07-14`, `${y}-08-15`, `${y}-11-01`, `${y}-11-11`, `${y}-12-25`
|
||||
];
|
||||
if (y === 2026) {
|
||||
holidays.push('2026-04-06');
|
||||
holidays.push('2026-05-14');
|
||||
holidays.push('2026-05-25');
|
||||
}
|
||||
return holidays.includes(s);
|
||||
};
|
||||
|
||||
const parseCirculationFlag = (val, defaultVal) => {
|
||||
if (val == null) return defaultVal;
|
||||
if (typeof val === 'boolean') return val;
|
||||
const s = String(val).trim().toLowerCase();
|
||||
if (['true', '1', 't', 'y', 'yes', 'oui'].includes(s)) return true;
|
||||
if (['false', '0', 'f', 'n', 'no', 'non'].includes(s)) return false;
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
const circulatesOn = (h, d) => {
|
||||
if (isPublicHoliday(d)) {
|
||||
return parseCirculationFlag(h['circule_jours_feries'], false);
|
||||
}
|
||||
const col = weekdayNameForColumn(d);
|
||||
if (!col) return true;
|
||||
return parseCirculationFlag(h[col], true);
|
||||
};
|
||||
|
||||
const parseDateOnly = (v) => {
|
||||
if (!v) return null;
|
||||
const d = new Date(v);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
};
|
||||
|
||||
const parseJoursTravaux = (val) => {
|
||||
if (val == null) return null;
|
||||
if (Array.isArray(val)) return new Set(val.map(x => String(x).toLowerCase()));
|
||||
const s = String(val || '');
|
||||
const parts = s.split(/[,;\s]+/).map(p => p.trim()).filter(Boolean);
|
||||
return parts.length ? new Set(parts.map(p => p.toLowerCase())) : null;
|
||||
};
|
||||
|
||||
const weekdayShort = (i) => {
|
||||
switch(i) { case 0: return 'dim'; case 1: return 'lun'; case 2: return 'mar'; case 3: return 'mer'; case 4: return 'jeu'; case 5: return 'ven'; case 6: return 'sam'; default: return null; }
|
||||
};
|
||||
|
||||
const travauxApplies = (t, h, date) => {
|
||||
if (!t) return false;
|
||||
const db = parseDateOnly(t.date_debut || t.start_date);
|
||||
const df = parseDateOnly(t.date_fin || t.end_date);
|
||||
if (!db || !df) return false;
|
||||
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
if (d < db || d > df) return false;
|
||||
const joursSet = parseJoursTravaux(t.jours_travaux || t.jours || t.days);
|
||||
if (joursSet) {
|
||||
const name = weekdayShort(d.getDay());
|
||||
if (!name) return false;
|
||||
let ok = false;
|
||||
for (const v of joursSet) { if (v.includes(name) || name.includes(v)) { ok = true; break; } }
|
||||
if (!ok) return false;
|
||||
}
|
||||
const startHM = parseTimeHM(t.heure_debut || t.start_time);
|
||||
const endHM = parseTimeHM(t.heure_fin || t.end_time);
|
||||
if (startHM == null || endHM == null) return true;
|
||||
if (typeof h._depHM === 'number') return h._depHM >= startHM && h._depHM <= endHM;
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSubstitution = (h) => {
|
||||
if (!h) return false;
|
||||
return substitutions.some(ts => String(ts.horaire_id) === String(h.id) || String(ts.horaire_id) === String(h.numero_train) || String(ts.horaire_id) === String(h.numero));
|
||||
};
|
||||
|
||||
const enriched = enrich.map(h => {
|
||||
let linked = false;
|
||||
let appliesToday = false;
|
||||
let appliesTomorrow = false;
|
||||
for (const t of travauxList) {
|
||||
const tLine = t.ligne_id || t.ligne || t.line_id || t.line;
|
||||
const hLine = h.ligne_id || h.ligne || h.line_id || h.line;
|
||||
if (typeof tLine !== 'undefined' && tLine !== null) {
|
||||
if (typeof hLine === 'undefined' || hLine === null || String(tLine) !== String(hLine)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const ref = t.horaire_id || t.horaire || t.numero_train || t.numero;
|
||||
if (ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero))) linked = true;
|
||||
const substForThisTravail = substitutions.some(ts => String(ts.travaux_id) === String(t.id) && (String(ts.horaire_id) === String(h.id) || String(ts.horaire_id) === String(h.numero_train) || String(ts.horaire_id) === String(h.numero)));
|
||||
if (substForThisTravail) linked = true;
|
||||
|
||||
if (!appliesToday && travauxApplies(t, h, today)) appliesToday = true;
|
||||
if (!appliesTomorrow && travauxApplies(t, h, tomorrow)) appliesTomorrow = true;
|
||||
if (linked && appliesToday && appliesTomorrow) break;
|
||||
}
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow };
|
||||
});
|
||||
|
||||
// Filter by station: keep arrivals to this station or stops at this station (excluding origin)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
// Exclude if target station is the departure origin
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
|
||||
// Include if target station is the terminus
|
||||
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (arriveeName === normalized) return true;
|
||||
|
||||
// Check intermediate stops
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
if (!stopName) continue;
|
||||
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
|
||||
if (isOrigin) continue;
|
||||
if (stopName === normalized) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Candidate filters for Today and Tomorrow (Arrival platform logic)
|
||||
const todayCandidates = stationFiltered.filter(h => {
|
||||
if (isSubstitution(h)) return Boolean(h._linkedToTravaux && h._travauxAppliesToday);
|
||||
if (h._date && h._date !== todayStr) return false;
|
||||
if (!circulatesOn(h, today)) return false;
|
||||
if (typeof h._depHM !== 'number') return false;
|
||||
// Keep arrivals that occur in the future or up to 1 minute ago (arrival window)
|
||||
if (h._depHM < todayHM - 1) return false;
|
||||
if (h._travauxAppliesToday) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const tomorrowCandidates = stationFiltered.filter(h => {
|
||||
if (isSubstitution(h)) return Boolean(h._linkedToTravaux && h._travauxAppliesTomorrow);
|
||||
if (h._date && h._date !== tomorrowStr) return false;
|
||||
if (!circulatesOn(h, tomorrow)) return false;
|
||||
if (h._travauxAppliesTomorrow) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
let finalList = [];
|
||||
let serviceDateStr = '';
|
||||
if (todayCandidates.length > 0) {
|
||||
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||
serviceDateStr = todayStr;
|
||||
} else {
|
||||
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||
serviceDateStr = tomorrowStr;
|
||||
}
|
||||
|
||||
// Apply perturbations (delays, cancellations)
|
||||
|
||||
const filteredPerturb = (perturbData || []).filter(p => {
|
||||
try {
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); };
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== serviceDateStr) return false;
|
||||
if (db && df && !(serviceDateStr >= db && serviceDateStr <= df)) return false;
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && !joursSet.has(serviceDateStr)) return false;
|
||||
return true;
|
||||
} catch (e) { return true; }
|
||||
});
|
||||
|
||||
const map = buildPerturbMap(filteredPerturb, parsedHoraires);
|
||||
const annotated = applyPerturbationsToHoraires(finalList, filteredPerturb, null, gareDisplay, serviceDateStr, map);
|
||||
|
||||
// Now filter by platform specific to this quai
|
||||
const candidatesOnPlatform = annotated.filter(h => {
|
||||
const stopsList = getStops(h);
|
||||
let stopForStation = null;
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
|
||||
});
|
||||
|
||||
if (candidatesOnPlatform.length > 0) {
|
||||
setNextArrival(candidatesOnPlatform[0]);
|
||||
} else {
|
||||
setNextArrival(null);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 15000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchData]);
|
||||
|
||||
if (loading) return <div className="afl-quais-arrivees__container" style={{background: '#1e4a30'}}></div>;
|
||||
if (!nextArrival) return (
|
||||
<div className="afl-quais-arrivees__container">
|
||||
<div className="afl-quais-arrivees__header">
|
||||
<div className="afl-quais-arrivees__header-left">
|
||||
<svg viewBox="0 0 24 24" width="6.5vh" height="6.5vh" fill="white" style={{ marginRight: '0.5vw' }}>
|
||||
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
|
||||
</svg>
|
||||
<div className="afl-quais-arrivees__header-title">Prochaine arrivée</div>
|
||||
</div>
|
||||
<div className="afl-quais-arrivees__header-clock">{formatTimeNoSeconds(currentTime)}</div>
|
||||
</div>
|
||||
<div className="afl-quais-arrivees__no-departure">Aucune arrivée prévue</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const stops = getStops(nextArrival);
|
||||
let filteredStops = [];
|
||||
|
||||
if (stops.length > 0 && gareDisplay) {
|
||||
const stationIndex = stops.findIndex(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
if (stationIndex !== -1) {
|
||||
filteredStops = stops.slice(0, stationIndex);
|
||||
} else {
|
||||
filteredStops = stops.slice();
|
||||
}
|
||||
} else {
|
||||
filteredStops = stops.slice();
|
||||
}
|
||||
|
||||
const originName = nextArrival.gare_depart_nom || nextArrival.gare_depart || 'Origine';
|
||||
if (filteredStops.length > 0) {
|
||||
const firstStopName = normalizeName((filteredStops[0].gare_nom || filteredStops[0].name || filteredStops[0].nom || '').toString());
|
||||
const normOrigin = normalizeName(originName);
|
||||
if (firstStopName !== normOrigin && !firstStopName.includes(normOrigin) && !normOrigin.includes(firstStopName)) {
|
||||
filteredStops.unshift({ gare_nom: originName, isFirst: true });
|
||||
} else {
|
||||
filteredStops[0].isFirst = true;
|
||||
}
|
||||
} else {
|
||||
filteredStops.unshift({ gare_nom: originName, isFirst: true });
|
||||
}
|
||||
|
||||
const targetStationName = gareDisplay || 'Terminus';
|
||||
filteredStops.push({ gare_nom: targetStationName, isLast: true });
|
||||
|
||||
// Deduplicate stops by name
|
||||
const seenStops = new Set();
|
||||
filteredStops = filteredStops.filter(s => {
|
||||
const name = normalizeName((s.gare_nom || s.name || s.nom || s.stop_name || '').toString());
|
||||
if (!name) return false;
|
||||
if (seenStops.has(name)) return false;
|
||||
seenStops.add(name);
|
||||
return true;
|
||||
});
|
||||
|
||||
const isSuppressed = nextArrival._status && nextArrival._status.label === 'supprimé';
|
||||
const isDelayed = nextArrival._status && nextArrival._status.label && String(nextArrival._status.label).toLowerCase().includes('retard');
|
||||
let displayStatusText = "À l'heure";
|
||||
if (isSuppressed) displayStatusText = "Supprimé";
|
||||
else if (isDelayed) {
|
||||
if (typeof nextArrival._status.delayMinutes === 'number') {
|
||||
displayStatusText = `+${nextArrival._status.delayMinutes} min`;
|
||||
} else {
|
||||
displayStatusText = nextArrival._status.label;
|
||||
}
|
||||
}
|
||||
|
||||
const displayTime = nextArrival._displayTime || formatTimeNoSeconds(nextArrival.heure_arrivee || nextArrival.time || nextArrival.heure || '');
|
||||
const trainType = nextArrival.type_train || nextArrival.op || 'TER';
|
||||
const trainNum = nextArrival.numero_train || nextArrival.numero || nextArrival.number || '';
|
||||
|
||||
const stopsList = getStops(nextArrival);
|
||||
let stopForStation = null;
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || nextArrival.quai_reel || nextArrival.voie || nextArrival.quai || quaiParam || '').toString().trim();
|
||||
|
||||
const shouldScroll = filteredStops.length > 10;
|
||||
const scrollStyle = shouldScroll ? {
|
||||
animation: `scrollStops ${filteredStops.length * 2.2}s linear infinite`
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<div className="afl-quais-arrivees__container">
|
||||
{/* Header */}
|
||||
<div className="afl-quais-arrivees__header">
|
||||
<div className="afl-quais-arrivees__header-left">
|
||||
<svg viewBox="0 0 24 24" width="6.5vh" height="6.5vh" fill="white" style={{ marginRight: '0.5vw' }}>
|
||||
<path d="M12 2c-4 0-8 .5-8 4v9.5C4 17.43 5.57 19 7.5 19L6 20.5v.5h12v-.5L16.5 19c1.93 0 3.5-1.57 3.5-3.5V6c0-3.5-4-4-8-4zM7.5 17c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm-4.5-5H6V7h12v5h-6z"/>
|
||||
</svg>
|
||||
<div className="afl-quais-arrivees__header-title">Prochaine arrivée</div>
|
||||
</div>
|
||||
<div className="afl-quais-arrivees__header-clock">{formatTimeNoSeconds(currentTime)}</div>
|
||||
</div>
|
||||
|
||||
<div className="afl-quais-arrivees__main">
|
||||
{/* Left Pane (Floating Card) */}
|
||||
<div className="afl-quais-arrivees__left-pane">
|
||||
<div className="afl-quais-arrivees__time-status">
|
||||
<div className="afl-quais-arrivees__time">{displayTime}</div>
|
||||
<div className={`afl-quais-arrivees__status-pill ${isSuppressed ? 'afl-quais-arrivees__status-pill--suppressed' : (isDelayed ? 'afl-quais-arrivees__status-pill--delayed' : '')}`}>
|
||||
{isSuppressed ? (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="#111" />
|
||||
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
|
||||
</svg>
|
||||
) : isDelayed ? (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="#111" />
|
||||
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="white" />
|
||||
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#216b3e" />
|
||||
</svg>
|
||||
)}
|
||||
<span>{displayStatusText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="afl-quais-arrivees__destination">{originName}</div>
|
||||
<div className="afl-quais-arrivees__train-info">
|
||||
<span className="afl-quais-arrivees__train-type">{trainType}</span>
|
||||
<span className="afl-quais-arrivees__train-number">{trainNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Pane (Timeline) */}
|
||||
<div className="afl-quais-arrivees__right-pane">
|
||||
<div className="afl-quais-arrivees__stops-window">
|
||||
<style>
|
||||
{`
|
||||
@keyframes scrollStops {
|
||||
0%, 15% { transform: translateY(0); }
|
||||
85%, 100% { transform: translateY(calc(-100% + 65vh)); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<ul className="afl-quais-arrivees__stops-list" style={shouldScroll ? { animation: `scrollStops ${filteredStops.length * 1.5}s ease-in-out infinite alternate` } : {}}>
|
||||
{/* Main List */}
|
||||
{filteredStops.map((s, i) => {
|
||||
const sname = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const isFirst = i === 0;
|
||||
const isLastItem = s.isLast || i === filteredStops.length - 1;
|
||||
|
||||
return (
|
||||
<li key={`s1-${i}`} className={`afl-quais-arrivees__stop-item ${isFirst ? 'is-first' : ''} ${isLastItem ? 'is-last' : ''}`}>
|
||||
<div className="afl-quais-arrivees__stop-indicator">
|
||||
<div className="afl-quais-arrivees__stop-line"></div>
|
||||
<div className={`afl-quais-arrivees__stop-dot ${isLastItem ? 'is-terminus' : ''}`}></div>
|
||||
</div>
|
||||
<div className={`afl-quais-arrivees__stop-name ${isLastItem ? 'is-terminus' : ''}`}>{sname}</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Symmetrical train outline under the stops list */}
|
||||
<div className="afl-quais-arrivees__bottom-container">
|
||||
<div className="afl-quais-arrivees__train-illustration">
|
||||
<svg viewBox="0 0 260 40" width="22vw" height="3.5vh">
|
||||
<path fill="none" stroke="#4caf50" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M30,35 L230,35 C242,35 248,31 254,22 L258,12 C260,8 256,4 247,4 L13,4 C4,4 0,8 2,12 L6,22 C12,31 18,35 30,35 Z" />
|
||||
<line x1="55" y1="4" x2="55" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="82" y1="4" x2="82" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="109" y1="4" x2="109" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="136" y1="4" x2="136" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="163" y1="4" x2="163" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="190" y1="4" x2="190" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
<line x1="217" y1="4" x2="217" y2="35" stroke="#4caf50" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AFLArriveesQuais;
|
||||
@@ -0,0 +1,382 @@
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.afl-departs__container {
|
||||
font-family: 'Achemine', Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #012a6a; /* Vivid dark blue background */
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.afl-departs__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #005eb8; /* Bright blue header */
|
||||
padding: 0 2vw;
|
||||
height: 10vh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.afl-departs__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5vw;
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.afl-departs__title {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.afl-departs__header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1vw;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.afl-departs__page-dot {
|
||||
width: 4vh;
|
||||
height: 4vh;
|
||||
border-radius: 50%;
|
||||
background-color: #012a6a; /* Dark blue for inactive */
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.afl-departs__page-dot--active {
|
||||
background-color: #00c3ff; /* Cyan for active */
|
||||
}
|
||||
|
||||
.afl-departs__header-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.afl-departs__header-right .aff-classiques-departs__clock-box {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.afl-departs__header-right .aff-classiques-departs__clock-main {
|
||||
font-size: 4.5vh;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.afl-departs__header-right .aff-classiques-departs__clock-seconds {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.afl-departs__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.afl-departs__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 2.5vh 2vw 0 2vw;
|
||||
position: relative;
|
||||
height: 17.5vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-departs__col-time-status {
|
||||
width: 16vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.afl-departs__time {
|
||||
font-size: 7vh;
|
||||
font-weight: 700;
|
||||
color: #fce205; /* Bright yellow */
|
||||
line-height: 1;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.afl-departs__time--suppressed {
|
||||
color: #ff4444;
|
||||
font-size: 4vh;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.afl-departs__status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #005eb8; /* Blue background matching header */
|
||||
border-radius: 3vh;
|
||||
padding: 0.6vh 0.8vw;
|
||||
margin-top: 1vh;
|
||||
font-size: 2.5vh;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.afl-departs__status-pill svg {
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
|
||||
.afl-departs__status-pill--delayed {
|
||||
background-color: #ff9e43;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-departs__status-pill--suppressed {
|
||||
background-color: #ff4c5b;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-departs__col-train-info {
|
||||
width: 12vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.5vh;
|
||||
align-items: flex-start; /* Left align type and number */
|
||||
}
|
||||
|
||||
.afl-departs__op-name {
|
||||
font-size: 3vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.afl-departs__op-number {
|
||||
font-size: 2.5vh;
|
||||
font-weight: 400;
|
||||
color: #cce0ff;
|
||||
margin-top: 0.5vh;
|
||||
}
|
||||
|
||||
.afl-departs__col-destination {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 2vw;
|
||||
padding-right: 2vw;
|
||||
padding-top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.afl-departs__dest-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
|
||||
.afl-departs__destination-name {
|
||||
font-size: 6vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.afl-departs__vias-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.afl-departs__vias-static {
|
||||
font-size: 3.5vh;
|
||||
font-weight: 400;
|
||||
color: #cce0ff;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.afl-departs__vias-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.afl-departs__vias {
|
||||
font-size: 3.5vh;
|
||||
font-weight: 400;
|
||||
color: #cce0ff;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
animation: afl-marquee 6.6s linear infinite;
|
||||
padding-left: 100%;
|
||||
}
|
||||
|
||||
@keyframes afl-marquee {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(-100%, 0); }
|
||||
}
|
||||
|
||||
.afl-departs__col-platform {
|
||||
width: 12vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.afl-departs__platform-box {
|
||||
background-color: white;
|
||||
border-radius: 0.8vh;
|
||||
width: 10vw;
|
||||
height: 12.5vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0.5vh 1vh rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.afl-departs__platform-label {
|
||||
color: #012a6a; /* Matches background */
|
||||
font-size: 2.8vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.afl-departs__platform-value {
|
||||
color: #012a6a; /* Matches background */
|
||||
font-size: 6.8vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.afl-departs__separator-wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.afl-departs__separator {
|
||||
position: absolute;
|
||||
height: 0.4vh;
|
||||
background-color: #005eb8;
|
||||
width: 96vw;
|
||||
border-radius: 0.2vh;
|
||||
}
|
||||
|
||||
.afl-departs__train-illustration {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: #012a6a; /* matches background */
|
||||
padding: 0 1vw;
|
||||
}
|
||||
|
||||
.afl-departs__train-illustration svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body:has(.afl-departs__container) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.afl-departs__container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-departs__separator-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.afl-departs__separator {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
background-color: #007ac3;
|
||||
width: calc(100% - 48px);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.afl-departs__train-illustration {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
background-color: #01347d; /* match row background so it overlays the line */
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.afl-departs__row--compact {
|
||||
height: 10.5vh;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.afl-departs__row--compact .afl-departs__platform-box {
|
||||
height: 8vh;
|
||||
}
|
||||
|
||||
.afl-departs__row--compact .afl-departs__platform-label {
|
||||
font-size: 1.8vh;
|
||||
}
|
||||
|
||||
.afl-departs__row--compact .afl-departs__platform-value {
|
||||
font-size: 4vh;
|
||||
}
|
||||
|
||||
.afl-departs__row--compact .afl-departs__col-time-status,
|
||||
.afl-departs__row--compact .afl-departs__col-train-info,
|
||||
.afl-departs__row--compact .afl-departs__col-destination,
|
||||
.afl-departs__row--compact .afl-departs__col-platform {
|
||||
padding-top: 0;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,335 @@
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.afl-quais-departs__container {
|
||||
--bg-color: #042054; /* Deep dark blue background */
|
||||
--header-bg: #083c84; /* Lighter vibrant blue header */
|
||||
--accent-cyan: #00c3e3; /* Cyan timeline color */
|
||||
--accent-yellow: #cce815; /* Bright chartreuse/lime-yellow for departure time */
|
||||
--text-grey: #a0b2d6; /* Light grey-blue for train info */
|
||||
}
|
||||
|
||||
.afl-quais-departs__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
color: white;
|
||||
font-family: 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* --- HEADER --- */
|
||||
.afl-quais-departs__header {
|
||||
height: 12vh;
|
||||
background-color: var(--header-bg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 3vw;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-departs__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2vw;
|
||||
}
|
||||
|
||||
.afl-quais-departs__header-title {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.afl-quais-departs__header-clock {
|
||||
font-size: 5vh;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* --- MAIN CONTENT --- */
|
||||
.afl-quais-departs__main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- LEFT PANE (GLUED TO LEFT/TOP, ROUNDED BOTTOM RIGHT) --- */
|
||||
.afl-quais-departs__left-pane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35vw;
|
||||
height: 38vh;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 0 0 24px 0; /* Only bottom-right is rounded */
|
||||
padding: 4vh 3vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.afl-quais-departs__time-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
}
|
||||
|
||||
.afl-quais-departs__time {
|
||||
font-size: 7.5vh;
|
||||
font-weight: 800;
|
||||
color: var(--accent-yellow);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.afl-quais-departs__status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #005eb8;
|
||||
border-radius: 3vh;
|
||||
padding: 0.6vh 0.8vw;
|
||||
font-size: 2.2vh;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.afl-quais-departs__status-pill svg {
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
|
||||
.afl-quais-departs__status-pill--delayed {
|
||||
background-color: #ff9e43;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-quais-departs__status-pill--suppressed {
|
||||
background-color: #ff4c5b;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.afl-quais-departs__destination {
|
||||
font-size: 5.5vh;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: #ffffff;
|
||||
margin-bottom: 1.5vh;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.afl-quais-departs__train-info {
|
||||
font-size: 3vh;
|
||||
color: var(--text-grey);
|
||||
display: flex;
|
||||
gap: 0.8vw;
|
||||
}
|
||||
|
||||
.afl-quais-departs__train-type {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.afl-quais-departs__train-number {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- RIGHT PANE (TIMELINE) --- */
|
||||
.afl-quais-departs__right-pane {
|
||||
position: absolute;
|
||||
top: 4vh;
|
||||
left: 38vw;
|
||||
width: 58vw;
|
||||
bottom: 12vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* Center stops list vertically */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.afl-quais-departs__stops-window {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.afl-quais-departs__stops-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.afl-quais-departs__stop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 6.2vh;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Timeline Indicator (Line segment + Dot) */
|
||||
.afl-quais-departs__stop-indicator {
|
||||
position: relative;
|
||||
width: 4vw;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.afl-quais-departs__stop-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.6vh;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 6px rgba(0, 195, 227, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Adjust line ends for first and last stops */
|
||||
.afl-quais-departs__stop-item.is-first .afl-quais-departs__stop-line {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.afl-quais-departs__stop-item.is-last .afl-quais-departs__stop-line {
|
||||
bottom: 50%;
|
||||
}
|
||||
|
||||
/* Standard dot */
|
||||
.afl-quais-departs__stop-dot {
|
||||
width: 1.4vh;
|
||||
height: 1.4vh;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 6px rgba(0, 195, 227, 0.6);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Terminus dot */
|
||||
.afl-quais-departs__stop-dot.is-terminus {
|
||||
width: 2.6vh;
|
||||
height: 2.6vh;
|
||||
background-color: var(--bg-color); /* Hollow effect */
|
||||
border: 0.6vh solid var(--accent-cyan);
|
||||
box-shadow: 0 0 8px rgba(0, 195, 227, 0.8);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,881 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import './AFLDepartsQuais.css';
|
||||
|
||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
const tryDate = new Date(v);
|
||||
if (!isNaN(tryDate.getTime())) {
|
||||
const hh = String(tryDate.getHours()).padStart(2, '0');
|
||||
const mm = String(tryDate.getMinutes()).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
const parts = String(v).split(':');
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const AFLDepartsQuais = () => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [nextDeparture, setNextDeparture] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Parse URL manually since we are not in a React Router context
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const gareParam = searchParams.get('gare') || '';
|
||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
|
||||
// Real-time clock update
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// --- Helpers pour perturbations, travaux, etc (portés de AFLDeparts.js) ---
|
||||
const normalizeName = useCallback((s) => {
|
||||
if (!s) return '';
|
||||
try {
|
||||
const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
|
||||
} catch (e) {
|
||||
return String(s).toLowerCase().trim();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const safeParseArrets = useCallback((p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
if (Array.isArray(parsed.arrets)) return parsed.arrets;
|
||||
return parsed;
|
||||
}
|
||||
return [];
|
||||
} catch (e) { return []; }
|
||||
}, []);
|
||||
|
||||
const getStops = useCallback((h) => {
|
||||
if (!h) return [];
|
||||
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
|
||||
if (Array.isArray(h.gares_desservies) && h.gares_desservies.length) return h.gares_desservies;
|
||||
if (Array.isArray(h.garesDesservies) && h.garesDesservies.length) return h.garesDesservies;
|
||||
const candidates = ['stops', 'gares_desservies', 'garesDesservies', 'gares_desservies_json'];
|
||||
for (const key of candidates) {
|
||||
const raw = h[key];
|
||||
if (!raw) continue;
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (typeof raw === 'string') {
|
||||
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const parsePerturbArretsObject = useCallback((p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return { arrets: [], jours: null };
|
||||
if (typeof raw === 'object') {
|
||||
if (Array.isArray(raw.arrets)) return { arrets: raw.arrets, jours: raw.jours_perturbation || raw.jours || null };
|
||||
if (Array.isArray(raw)) return { arrets: raw, jours: null };
|
||||
return { arrets: [], jours: null };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return { arrets: Array.isArray(parsed.arrets) ? parsed.arrets : (Array.isArray(parsed) ? parsed : []), jours: parsed.jours_perturbation || parsed.jours || null };
|
||||
}
|
||||
return { arrets: [], jours: null };
|
||||
} catch (e) {
|
||||
return { arrets: [], jours: null };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPerturbationJours = useCallback((p) => {
|
||||
if (!p) return null;
|
||||
const candidate = p.jours_perturbation || p.jours || null;
|
||||
const result = new Set();
|
||||
const pushDate = (d) => {
|
||||
if (!d) return;
|
||||
try {
|
||||
const dd = new Date(d);
|
||||
if (!isNaN(dd.getTime())) result.add(dd.toISOString().slice(0, 10));
|
||||
} catch (e) {}
|
||||
};
|
||||
if (candidate) {
|
||||
if (Array.isArray(candidate)) {
|
||||
candidate.forEach(item => pushDate(item.date || item));
|
||||
} else if (typeof candidate === 'string') {
|
||||
try { const parsed = JSON.parse(candidate); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); }
|
||||
catch (e) { pushDate(candidate); }
|
||||
} else if (typeof candidate === 'object') {
|
||||
if (Array.isArray(candidate)) candidate.forEach(it => pushDate(it.date || it));
|
||||
else pushDate(candidate.date || null);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const parsedObj = parsePerturbArretsObject(p);
|
||||
const jp = parsedObj.jours;
|
||||
if (jp) {
|
||||
if (Array.isArray(jp)) jp.forEach(it => pushDate(it.date || it));
|
||||
else if (typeof jp === 'string') {
|
||||
try { const parsed = JSON.parse(jp); if (Array.isArray(parsed)) parsed.forEach(it => pushDate(it.date || it)); } catch (e) { pushDate(jp); }
|
||||
} else if (typeof jp === 'object') pushDate(jp.date || null);
|
||||
}
|
||||
} catch (e) {}
|
||||
return result.size ? result : null;
|
||||
}, [parsePerturbArretsObject]);
|
||||
|
||||
const parseDelayMinutesFromArret = useCallback((arret) => {
|
||||
if (!arret) return null;
|
||||
const keys = ['retard_minutes', 'retard', 'delay_minutes', 'delay', 'retard_min'];
|
||||
for (const k of keys) {
|
||||
if (typeof arret[k] !== 'undefined' && arret[k] !== null && arret[k] !== '') {
|
||||
const n = Number(arret[k]);
|
||||
if (!isNaN(n)) return Math.round(n);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const parseTimeHM = useCallback((s) => {
|
||||
if (!s) return null;
|
||||
if (typeof s === 'number') return s;
|
||||
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
|
||||
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const buildPerturbMap = useCallback((list, allHorairesList) => {
|
||||
const idx = { byHoraire: {}, byNumber: {}, byStopId: {}, byStopName: {}, global: [] };
|
||||
(list || []).forEach(p => {
|
||||
const horaireId = p.horaire_id || p.horaire || p.horaireId || p.horaireID;
|
||||
if (horaireId != null) { const k = String(horaireId); idx.byHoraire[k] = idx.byHoraire[k] || []; idx.byHoraire[k].push(p); }
|
||||
const number = p.number || p.train_number || p.train || p.numero || p.numero_train;
|
||||
if (number != null) { const kn = String(number); idx.byNumber[kn] = idx.byNumber[kn] || []; idx.byNumber[kn].push(p); }
|
||||
const arrets = safeParseArrets(p);
|
||||
if (Array.isArray(arrets) && arrets.length) {
|
||||
arrets.forEach(a => {
|
||||
const stopId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code_uic || a.gare_uic || a.code;
|
||||
const stopName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
if (stopId != null) { const ks = String(stopId); idx.byStopId[ks] = idx.byStopId[ks] || []; idx.byStopId[ks].push({ perturbation: p, arret: a }); }
|
||||
if (stopName) { const key = normalizeName(stopName); idx.byStopName[key] = idx.byStopName[key] || []; idx.byStopName[key].push({ perturbation: p, arret: a }); }
|
||||
});
|
||||
}
|
||||
if (horaireId == null && number == null && (!Array.isArray(arrets) || !arrets.length)) { idx.global.push(p); }
|
||||
});
|
||||
if (Array.isArray(allHorairesList) && allHorairesList.length) {
|
||||
try {
|
||||
const byH = Object.keys(idx.byHoraire || {});
|
||||
if (byH && byH.length) {
|
||||
for (const hid of byH) {
|
||||
const h = allHorairesList.find(x => String(x.id) === String(hid));
|
||||
if (h) {
|
||||
const num = h.numero_train || h.number || h.numero || null;
|
||||
if (num != null) {
|
||||
const numKey = String(num);
|
||||
idx.byNumber[numKey] = idx.byNumber[numKey] || [];
|
||||
(idx.byHoraire[hid] || []).forEach(p => idx.byNumber[numKey].push(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return idx;
|
||||
}, [safeParseArrets, normalizeName]);
|
||||
|
||||
|
||||
|
||||
const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
|
||||
const map = idx || buildPerturbMap();
|
||||
const allPerturbs = [];
|
||||
const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train);
|
||||
const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number);
|
||||
|
||||
if (map) {
|
||||
const addedIds = new Set();
|
||||
const addP = (p) => {
|
||||
if (p && !addedIds.has(p.id)) {
|
||||
addedIds.add(p.id);
|
||||
allPerturbs.push(p);
|
||||
}
|
||||
};
|
||||
if (hId != null && map.byHoraire && map.byHoraire[String(hId)]) {
|
||||
map.byHoraire[String(hId)].forEach(addP);
|
||||
}
|
||||
if (number != null && map.byNumber && map.byNumber[String(number)]) {
|
||||
map.byNumber[String(number)].forEach(addP);
|
||||
}
|
||||
if (stationId != null && map.byStopId && map.byStopId[String(stationId)]) {
|
||||
map.byStopId[String(stationId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (stationName && map.byStopName) {
|
||||
const key = normalizeName(stationName);
|
||||
if (map.byStopName[key]) map.byStopName[key].forEach(x => addP(x.perturbation));
|
||||
for (const existingKey of Object.keys(map.byStopName)) {
|
||||
if (existingKey.includes(key) || key.includes(existingKey)) {
|
||||
map.byStopName[existingKey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
if (Array.isArray(trainStops)) {
|
||||
trainStops.forEach(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && map.byStopId && map.byStopId[String(sId)]) {
|
||||
map.byStopId[String(sId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (sName && map.byStopName) {
|
||||
const skey = normalizeName(sName);
|
||||
if (map.byStopName[skey]) map.byStopName[skey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (map.global) {
|
||||
map.global.forEach(addP);
|
||||
}
|
||||
}
|
||||
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); };
|
||||
let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
|
||||
if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
|
||||
|
||||
let maxDelay = null;
|
||||
let isSuppressed = false;
|
||||
|
||||
for (const p of allPerturbs) {
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== horaireYmd) continue;
|
||||
if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && horaireYmd && !joursSet.has(horaireYmd)) continue;
|
||||
} catch (e) {}
|
||||
|
||||
const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
|
||||
const numberRef = p.number || p.train_number || p.numero || p.numero_train;
|
||||
const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
|
||||
const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
|
||||
|
||||
const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
|
||||
const arrets = parsedObj.arrets || [];
|
||||
|
||||
const trainStops = getStops(horaire);
|
||||
let trainStopMatch = false;
|
||||
|
||||
const matchesTarget = (a) => {
|
||||
if (!stationName && stationId == null) return false;
|
||||
const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
if (stationId != null && String(aId) === String(stationId)) return true;
|
||||
if (stationName && aName) {
|
||||
const na = normalizeName(aName);
|
||||
const ns = normalizeName(stationName);
|
||||
return na === ns || na.includes(ns) || ns.includes(na);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const a of arrets) {
|
||||
const aId = a.gare_id || a.gareId || a.id || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
const na = normalizeName(aName || '');
|
||||
|
||||
const isTarget = matchesTarget(a);
|
||||
let isTrainStop = false;
|
||||
if (trainStops && Array.isArray(trainStops)) {
|
||||
isTrainStop = trainStops.some(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && aId != null && String(sId) === String(aId)) return true;
|
||||
if (sName && aName) {
|
||||
const ns = normalizeName(sName);
|
||||
return ns === na || ns.includes(na) || na.includes(ns);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const originName = normalizeName(horaire.gare_depart_nom || horaire.gare_depart || '');
|
||||
const destName = normalizeName(horaire.gare_arrivee_nom || horaire.gare_arrivee || '');
|
||||
if (originName === na || destName === na) isTrainStop = true;
|
||||
|
||||
if (isTrainStop) trainStopMatch = true;
|
||||
|
||||
if (isTarget) {
|
||||
const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = (arrets.length === 0) && !ref && !numberRef;
|
||||
const applies = appliesByRef || appliesByNumber || trainStopMatch || isGlobal;
|
||||
|
||||
if (applies) {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
|
||||
for (const a of arrets) {
|
||||
const tType = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (tType.includes('retard') || tType.includes('delay') || /retard/.test(tType)) {
|
||||
const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
const applyPerturbationsToHoraires = useCallback((list, perturbList, displayStationId, displayStationName, serviceDate, map) => {
|
||||
const targetStationId = displayStationId || null;
|
||||
const targetStationName = displayStationName || null;
|
||||
|
||||
return (list || []).map(h => {
|
||||
const horaire = { ...h };
|
||||
const stops = getStops(horaire);
|
||||
const stopsStatus = [];
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const s = stops[i] || {};
|
||||
const stationId = s.gare_id || s.gareId || s.id || s.station_id || s.stationId || s.uic || s.code;
|
||||
const stationName = s.gare_nom || s.nom || s.name || s.station_name || s.station_nom;
|
||||
const st = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
|
||||
stopsStatus.push({ stationId, stationName, status: st });
|
||||
}
|
||||
|
||||
const displayStatus = computeStatusForHoraireAtStop(horaire, targetStationId, targetStationName, map, serviceDate);
|
||||
let displayTime = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
horaire._stopsStatus = stopsStatus;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
return horaire;
|
||||
});
|
||||
}, [computeStatusForHoraireAtStop, getStops]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
|
||||
supabase.from('horaires').select('*'),
|
||||
supabase.from('travaux_substitutions').select('*'),
|
||||
supabase.from('perturbations').select('*'),
|
||||
supabase.from('travaux').select('*')
|
||||
]);
|
||||
|
||||
let parsedHoraires = (horairesData || []).map(h => {
|
||||
try {
|
||||
if (h && typeof h.stops === 'string') {
|
||||
const parsed = JSON.parse(h.stops);
|
||||
if (Array.isArray(parsed)) return { ...h, stops: parsed };
|
||||
}
|
||||
} catch (e) {}
|
||||
return h || {};
|
||||
});
|
||||
|
||||
const substitutions = subData || [];
|
||||
const travauxList = travauxData || [];
|
||||
|
||||
// Link substitutions
|
||||
parsedHoraires = parsedHoraires.map(h => {
|
||||
const subs = substitutions.filter(s => String(s.horaire_id) === String(h.id));
|
||||
if (subs.length > 0) {
|
||||
return { ...h, _substitutions: subs };
|
||||
}
|
||||
return h;
|
||||
});
|
||||
|
||||
// Normaliser date
|
||||
const dateKeys = ['date_depart', 'date', 'jour', 'service_date', 'date_service', 'date_dep'];
|
||||
const normalizeDate = (o) => {
|
||||
for (const k of dateKeys) {
|
||||
if (!o) continue;
|
||||
const v = o[k];
|
||||
if (!v) continue;
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const withDates = parsedHoraires.map(h => ({ ...h, _date: normalizeDate(h) }));
|
||||
|
||||
// Time parsing & enrichment
|
||||
const now = new Date();
|
||||
const todayHM = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const enrich = withDates.map(h => {
|
||||
let raw = h.heure_depart || h.time || h.heure || '';
|
||||
const stopsListForH = getStops(h);
|
||||
if (gareDisplay && Array.isArray(stopsListForH)) {
|
||||
const stop = stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
|
||||
}
|
||||
const depHM = parseTimeHM(raw);
|
||||
const displayTime = formatTimeNoSeconds(raw);
|
||||
return { ...h, _rawTime: raw, _depHM: depHM, _displayTime: displayTime };
|
||||
});
|
||||
|
||||
// Jours de circulation
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const localDateStr = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
const today = new Date();
|
||||
const todayStr = localDateStr(today);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
const tomorrowStr = localDateStr(tomorrow);
|
||||
|
||||
const weekdayIndex = (d) => d.getDay();
|
||||
const weekdayNameForColumn = (d) => {
|
||||
const i = weekdayIndex(d);
|
||||
switch (i) {
|
||||
case 1: return 'circule_lundi';
|
||||
case 2: return 'circule_mardi';
|
||||
case 3: return 'circule_mercredi';
|
||||
case 4: return 'circule_jeudi';
|
||||
case 5: return 'circule_vendredi';
|
||||
case 6: return 'circule_samedi';
|
||||
case 0: return 'circule_dimanche';
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isPublicHoliday = (d) => {
|
||||
const y = d.getFullYear();
|
||||
const m = d.getMonth() + 1;
|
||||
const day = d.getDate();
|
||||
const s = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const holidays = [
|
||||
`${y}-01-01`, `${y}-05-01`, `${y}-05-08`, `${y}-07-14`, `${y}-08-15`, `${y}-11-01`, `${y}-11-11`, `${y}-12-25`
|
||||
];
|
||||
if (y === 2026) {
|
||||
holidays.push('2026-04-06');
|
||||
holidays.push('2026-05-14');
|
||||
holidays.push('2026-05-25');
|
||||
}
|
||||
return holidays.includes(s);
|
||||
};
|
||||
|
||||
const parseCirculationFlag = (val, defaultVal) => {
|
||||
if (val == null) return defaultVal;
|
||||
if (typeof val === 'boolean') return val;
|
||||
const s = String(val).trim().toLowerCase();
|
||||
if (['true', '1', 't', 'y', 'yes', 'oui'].includes(s)) return true;
|
||||
if (['false', '0', 'f', 'n', 'no', 'non'].includes(s)) return false;
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
const circulatesOn = (h, d) => {
|
||||
if (isPublicHoliday(d)) {
|
||||
return parseCirculationFlag(h['circule_jours_feries'], false);
|
||||
}
|
||||
const col = weekdayNameForColumn(d);
|
||||
if (!col) return true;
|
||||
return parseCirculationFlag(h[col], true);
|
||||
};
|
||||
|
||||
const parseDateOnly = (v) => {
|
||||
if (!v) return null;
|
||||
const d = new Date(v);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
};
|
||||
|
||||
const parseJoursTravaux = (val) => {
|
||||
if (val == null) return null;
|
||||
if (Array.isArray(val)) return new Set(val.map(x => String(x).toLowerCase()));
|
||||
const s = String(val || '');
|
||||
const parts = s.split(/[,;\s]+/).map(p => p.trim()).filter(Boolean);
|
||||
return parts.length ? new Set(parts.map(p => p.toLowerCase())) : null;
|
||||
};
|
||||
|
||||
const weekdayShort = (i) => {
|
||||
switch(i) { case 0: return 'dim'; case 1: return 'lun'; case 2: return 'mar'; case 3: return 'mer'; case 4: return 'jeu'; case 5: return 'ven'; case 6: return 'sam'; default: return null; }
|
||||
};
|
||||
|
||||
const travauxApplies = (t, h, date) => {
|
||||
if (!t) return false;
|
||||
const db = parseDateOnly(t.date_debut || t.start_date);
|
||||
const df = parseDateOnly(t.date_fin || t.end_date);
|
||||
if (!db || !df) return false;
|
||||
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
if (d < db || d > df) return false;
|
||||
const joursSet = parseJoursTravaux(t.jours_travaux || t.jours || t.days);
|
||||
if (joursSet) {
|
||||
const name = weekdayShort(d.getDay());
|
||||
if (!name) return false;
|
||||
let ok = false;
|
||||
for (const v of joursSet) { if (v.includes(name) || name.includes(v)) { ok = true; break; } }
|
||||
if (!ok) return false;
|
||||
}
|
||||
const startHM = parseTimeHM(t.heure_debut || t.start_time);
|
||||
const endHM = parseTimeHM(t.heure_fin || t.end_time);
|
||||
if (startHM == null || endHM == null) return true;
|
||||
if (typeof h._depHM === 'number') return h._depHM >= startHM && h._depHM <= endHM;
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSubstitution = (h) => {
|
||||
if (!h) return false;
|
||||
return substitutions.some(ts => String(ts.horaire_id) === String(h.id) || String(ts.horaire_id) === String(h.numero_train) || String(ts.horaire_id) === String(h.numero));
|
||||
};
|
||||
|
||||
const enriched = enrich.map(h => {
|
||||
let linked = false;
|
||||
let appliesToday = false;
|
||||
let appliesTomorrow = false;
|
||||
for (const t of travauxList) {
|
||||
const tLine = t.ligne_id || t.ligne || t.line_id || t.line;
|
||||
const hLine = h.ligne_id || h.ligne || h.line_id || h.line;
|
||||
if (typeof tLine !== 'undefined' && tLine !== null) {
|
||||
if (typeof hLine === 'undefined' || hLine === null || String(tLine) !== String(hLine)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const ref = t.horaire_id || t.horaire || t.numero_train || t.numero;
|
||||
if (ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero))) linked = true;
|
||||
const substForThisTravail = substitutions.some(ts => String(ts.travaux_id) === String(t.id) && (String(ts.horaire_id) === String(h.id) || String(ts.horaire_id) === String(h.numero_train) || String(ts.horaire_id) === String(h.numero)));
|
||||
if (substForThisTravail) linked = true;
|
||||
|
||||
if (!appliesToday && travauxApplies(t, h, today)) appliesToday = true;
|
||||
if (!appliesTomorrow && travauxApplies(t, h, tomorrow)) appliesTomorrow = true;
|
||||
if (linked && appliesToday && appliesTomorrow) break;
|
||||
}
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow };
|
||||
});
|
||||
|
||||
// Filter by station: keep departures from this station or stops at this station (excluding terminus)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
let maxIdx = null;
|
||||
for (const st of stopsList) {
|
||||
if (typeof st.index !== 'undefined' && st.index !== null) {
|
||||
const n = Number(st.index);
|
||||
if (!Number.isNaN(n)) maxIdx = maxIdx == null ? n : Math.max(maxIdx, n);
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Candidate filters for Today and Tomorrow (Departure platform logic)
|
||||
const todayCandidates = stationFiltered.filter(h => {
|
||||
if (isSubstitution(h)) return Boolean(h._linkedToTravaux && h._travauxAppliesToday);
|
||||
if (h._date && h._date !== todayStr) return false;
|
||||
if (!circulatesOn(h, today)) return false;
|
||||
if (typeof h._depHM !== 'number') return false;
|
||||
// Keep departures that occur in the future or up to 1 minute ago (departure window)
|
||||
if (h._depHM < todayHM - 1) return false;
|
||||
if (h._travauxAppliesToday) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const tomorrowCandidates = stationFiltered.filter(h => {
|
||||
if (isSubstitution(h)) return Boolean(h._linkedToTravaux && h._travauxAppliesTomorrow);
|
||||
if (h._date && h._date !== tomorrowStr) return false;
|
||||
if (!circulatesOn(h, tomorrow)) return false;
|
||||
if (h._travauxAppliesTomorrow) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
let finalList = [];
|
||||
let serviceDateStr = '';
|
||||
if (todayCandidates.length > 0) {
|
||||
finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||
serviceDateStr = todayStr;
|
||||
} else {
|
||||
finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM);
|
||||
serviceDateStr = tomorrowStr;
|
||||
}
|
||||
|
||||
// Apply perturbations (delays, cancellations)
|
||||
|
||||
const filteredPerturb = (perturbData || []).filter(p => {
|
||||
try {
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); };
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== serviceDateStr) return false;
|
||||
if (db && df && !(serviceDateStr >= db && serviceDateStr <= df)) return false;
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && !joursSet.has(serviceDateStr)) return false;
|
||||
return true;
|
||||
} catch (e) { return true; }
|
||||
});
|
||||
|
||||
const map = buildPerturbMap(filteredPerturb, parsedHoraires);
|
||||
const annotated = applyPerturbationsToHoraires(finalList, filteredPerturb, null, gareDisplay, serviceDateStr, map);
|
||||
|
||||
// Now filter by platform specific to this quai
|
||||
const candidatesOnPlatform = annotated.filter(h => {
|
||||
const stopsList = getStops(h);
|
||||
let stopForStation = null;
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
|
||||
});
|
||||
|
||||
if (candidatesOnPlatform.length > 0) {
|
||||
setNextDeparture(candidatesOnPlatform[0]);
|
||||
} else {
|
||||
setNextDeparture(null);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 15000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchData]);
|
||||
|
||||
if (loading) return <div className="afl-quais-departs__container" style={{background: '#042054'}}></div>;
|
||||
if (!nextDeparture) return (
|
||||
<div className="afl-quais-departs__container">
|
||||
<div className="afl-quais-departs__header">
|
||||
<div className="afl-quais-departs__header-left">
|
||||
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
|
||||
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
|
||||
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
|
||||
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
</svg>
|
||||
<div className="afl-quais-departs__header-title">Prochain départ</div>
|
||||
</div>
|
||||
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
|
||||
</div>
|
||||
<div className="afl-quais-departs__no-departure">Aucun départ prévu</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const stops = getStops(nextDeparture);
|
||||
let filteredStops = [];
|
||||
|
||||
if (stops.length > 0 && gareDisplay) {
|
||||
const stationIndex = stops.findIndex(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
if (stationIndex !== -1) {
|
||||
filteredStops = stops.slice(stationIndex + 1);
|
||||
} else {
|
||||
const firstStopName = normalizeName((stops[0].gare_nom || stops[0].name || stops[0].nom || '').toString());
|
||||
if (firstStopName && firstStopName === normalizeName(gareDisplay)) {
|
||||
filteredStops = stops.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const destName = nextDeparture.gare_arrivee_nom || nextDeparture.gare_arrivee || 'Terminus';
|
||||
if (filteredStops.length > 0) {
|
||||
const lastStopName = normalizeName((filteredStops[filteredStops.length - 1].gare_nom || filteredStops[filteredStops.length - 1].name || filteredStops[filteredStops.length - 1].nom || '').toString());
|
||||
const normDest = normalizeName(destName);
|
||||
if (lastStopName !== normDest && !lastStopName.includes(normDest) && !normDest.includes(lastStopName)) {
|
||||
filteredStops.push({ gare_nom: destName, isLast: true });
|
||||
} else {
|
||||
filteredStops[filteredStops.length - 1].isLast = true;
|
||||
}
|
||||
} else {
|
||||
filteredStops.push({ gare_nom: destName, isLast: true });
|
||||
}
|
||||
|
||||
// Deduplicate stops by name
|
||||
const seenStops = new Set();
|
||||
filteredStops = filteredStops.filter(s => {
|
||||
const name = normalizeName((s.gare_nom || s.name || s.nom || s.stop_name || '').toString());
|
||||
if (!name) return false;
|
||||
if (seenStops.has(name)) return false;
|
||||
seenStops.add(name);
|
||||
return true;
|
||||
});
|
||||
|
||||
const isSuppressed = nextDeparture._status && nextDeparture._status.label === 'supprimé';
|
||||
const isDelayed = nextDeparture._status && nextDeparture._status.label && String(nextDeparture._status.label).toLowerCase().includes('retard');
|
||||
let displayStatusText = "À l'heure";
|
||||
if (isSuppressed) displayStatusText = "Supprimé";
|
||||
else if (isDelayed) {
|
||||
if (typeof nextDeparture._status.delayMinutes === 'number') {
|
||||
displayStatusText = `+${nextDeparture._status.delayMinutes} min`;
|
||||
} else {
|
||||
displayStatusText = nextDeparture._status.label;
|
||||
}
|
||||
}
|
||||
|
||||
const displayTime = nextDeparture._displayTime || formatTimeNoSeconds(nextDeparture.heure_depart || nextDeparture.time || nextDeparture.heure || '');
|
||||
const trainType = nextDeparture.type_train || nextDeparture.op || 'TER';
|
||||
const trainNum = nextDeparture.numero_train || nextDeparture.numero || nextDeparture.number || '';
|
||||
|
||||
const stopsList = getStops(nextDeparture);
|
||||
let stopForStation = null;
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || nextDeparture.quai_reel || nextDeparture.voie || nextDeparture.quai || quaiParam || '').toString().trim();
|
||||
|
||||
const shouldScroll = filteredStops.length > 10;
|
||||
const scrollStyle = shouldScroll ? {
|
||||
animation: `scrollStops ${filteredStops.length * 2.2}s linear infinite`
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<div className="afl-quais-departs__container">
|
||||
{/* Header */}
|
||||
<div className="afl-quais-departs__header">
|
||||
<div className="afl-quais-departs__header-left">
|
||||
<svg viewBox="0 0 44 24" width="6.5vh" height="4.5vh" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '0.5vw' }}>
|
||||
{/* Chevrons / motion lines */}
|
||||
<path d="M4 6l6 6-6 6M12 6l6 6-6 6" stroke="white" strokeWidth="2.5" opacity="0.75" />
|
||||
{/* Train icon */}
|
||||
<path d="M22 17h16c1.5 0 2.5-1 2.5-2.5v-5C40.5 8 39.5 7 38 7H22c-1.5 0-2.5 1-2.5 2.5v5c0 1.5 1 2.5 2.5 2.5z" fill="white" stroke="none" />
|
||||
<rect x="23" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
<rect x="28" y="9" width="3" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
<rect x="33" y="9" width="4" height="4" fill="#083c84" stroke="none" rx="0.5" />
|
||||
</svg>
|
||||
<div className="afl-quais-departs__header-title">Prochain départ</div>
|
||||
</div>
|
||||
<div className="afl-quais-departs__header-clock">{formatTimeNoSeconds(currentTime)}</div>
|
||||
</div>
|
||||
|
||||
<div className="afl-quais-departs__main">
|
||||
{/* Left Pane (Floating Card) */}
|
||||
<div className="afl-quais-departs__left-pane">
|
||||
<div className="afl-quais-departs__time-status">
|
||||
<div className="afl-quais-departs__time">{displayTime}</div>
|
||||
<div className={`afl-quais-departs__status-pill ${isSuppressed ? 'afl-quais-departs__status-pill--suppressed' : (isDelayed ? 'afl-quais-departs__status-pill--delayed' : '')}`}>
|
||||
{isSuppressed ? (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="#111" />
|
||||
<path d="M16 9l-1.5-1.5L12 10.5 9.5 8 8 9.5l2.5 2.5L8 14.5 9.5 16l2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5z" fill="#ff4c5b" />
|
||||
</svg>
|
||||
) : isDelayed ? (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="#111" />
|
||||
<path d="M11 6h2v6h-2zm0 8h2v2h-2z" fill="#ff9e43" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" width="2.5vh" height="2.5vh" style={{ marginRight: '0.5vw' }}>
|
||||
<circle cx="12" cy="12" r="11" fill="white" />
|
||||
<path d="M10 16.5l-4-4 1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5-8 8z" fill="#005eb8" />
|
||||
</svg>
|
||||
)}
|
||||
<span>{displayStatusText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="afl-quais-departs__destination">{destName}</div>
|
||||
<div className="afl-quais-departs__train-info">
|
||||
<span className="afl-quais-departs__train-type">{trainType}</span>
|
||||
<span className="afl-quais-departs__train-number">{trainNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Pane (Timeline) */}
|
||||
<div className="afl-quais-departs__right-pane">
|
||||
<div className="afl-quais-departs__stops-window">
|
||||
<style>
|
||||
{`
|
||||
@keyframes scrollStops {
|
||||
0%, 15% { transform: translateY(0); }
|
||||
85%, 100% { transform: translateY(calc(-100% + 65vh)); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<ul className="afl-quais-departs__stops-list" style={shouldScroll ? { animation: `scrollStops ${filteredStops.length * 1.5}s ease-in-out infinite alternate` } : {}}>
|
||||
{/* Main List */}
|
||||
{filteredStops.map((s, i) => {
|
||||
const sname = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const isFirst = !shouldScroll && i === 0;
|
||||
const isLastItem = s.isLast || i === filteredStops.length - 1;
|
||||
const isLast = !shouldScroll && isLastItem;
|
||||
|
||||
return (
|
||||
<li key={`s1-${i}`} className={`afl-quais-departs__stop-item ${isFirst ? 'is-first' : ''} ${isLast ? 'is-last' : ''}`}>
|
||||
<div className="afl-quais-departs__stop-indicator">
|
||||
<div className="afl-quais-departs__stop-line"></div>
|
||||
<div className={`afl-quais-departs__stop-dot ${isLastItem ? 'is-terminus' : ''}`}></div>
|
||||
</div>
|
||||
<div className={`afl-quais-departs__stop-name ${isLastItem ? 'is-terminus' : ''}`}>{sname}</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Symmetrical train outline under the stops list */}
|
||||
<div className="afl-quais-departs__bottom-container">
|
||||
<div className="afl-quais-departs__train-illustration">
|
||||
<svg viewBox="0 0 260 40" width="22vw" height="3.5vh">
|
||||
<path fill="none" stroke="#00c3e3" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M30,35 L230,35 C242,35 248,31 254,22 L258,12 C260,8 256,4 247,4 L13,4 C4,4 0,8 2,12 L6,22 C12,31 18,35 30,35 Z" />
|
||||
<line x1="55" y1="4" x2="55" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="82" y1="4" x2="82" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="109" y1="4" x2="109" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="136" y1="4" x2="136" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="163" y1="4" x2="163" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="190" y1="4" x2="190" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
<line x1="217" y1="4" x2="217" y2="35" stroke="#00c3e3" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
export default AFLDepartsQuais;
|
||||
@@ -1,17 +1,6 @@
|
||||
/* Copié depuis ClassiquesDeparts.css — styles identiques pour les arrivées */
|
||||
|
||||
:where(.classiques-arrivees-container, .aff-quais__container--arrivees) {
|
||||
/* variables locales aux composants Arrivées (ne pas définir sur :root) */
|
||||
--green-dark: #2d522c; /* bande foncée */
|
||||
--green: #38723c; /* bande claire */
|
||||
--accent-yellow: #e1c14c; /* heure */
|
||||
--white: #f3f3f3;
|
||||
--rows: 9; /* 9 horaires visibles + 1 footer */
|
||||
--footer-height: 100px; /* hauteur de la dernière 'ligne' servant de footer */
|
||||
--watermark-green: rgb(75, 154, 84);
|
||||
}
|
||||
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
Fontes Achemine
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
@@ -34,365 +23,400 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.classiques-arrivees-container{
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
ANIMATIONS
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Info cause perturbation — slide-in de droite vers la gauche */
|
||||
@keyframes infoWrapperGrow {
|
||||
from { max-width: 0; }
|
||||
to { max-width: 30vw; }
|
||||
}
|
||||
|
||||
@keyframes infoSlideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
40% { opacity: 1; }
|
||||
to { transform: translateX(0%); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
CONTENEUR PRINCIPAL
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__container {
|
||||
/* Couleurs inspirées du vrai afficheur SNCF */
|
||||
--yellow: #FFCC00;
|
||||
--blue-bg: #1e4a30;
|
||||
--blue-odd: #216b3e;
|
||||
--blue-even: #16552a;
|
||||
--text-white: #ffffff;
|
||||
--text-muted: #7EE0A0;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
/* autoriser l'affichage des boîtes agrandies qui débordent des lignes */
|
||||
overflow: visible;
|
||||
font-family: "Achemine", "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--green-dark);
|
||||
/* plus de padding-bottom : le footer sera superposé en position absolute */
|
||||
}
|
||||
|
||||
.station-box-arrivees{
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
top: 12px;
|
||||
color: #ffffff;
|
||||
background: rgba(0,0,0,0.08);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.classiques-arrivees-container{
|
||||
border-bottom-right-radius: 10px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.arrivees-list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* La liste occupe toute la hauteur de l'écran; le footer sera affiché
|
||||
en position absolue par-dessus la partie inférieure. */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.arrivee-row{
|
||||
display: flex;
|
||||
/* laisser les colonnes occuper toute la hauteur de la ligne, puis centrer leur contenu */
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
padding: 0 1.2vw;
|
||||
/* Répartit l'espace en var(--rows) parts ; on utilise 100% / var(--rows) pour que
|
||||
9 lignes + 1 footer occupent l'écran de façon uniforme. */
|
||||
flex: 0 0 calc(100% / var(--rows));
|
||||
min-height: 50px; /* garde une bonne hauteur sur petits écrans */
|
||||
max-height: 120px;
|
||||
position: relative; /* pour positionner la .platform-box en absolu */
|
||||
overflow: visible; /* laisser la platform-box déborder si agrandie */
|
||||
}
|
||||
|
||||
.arrivee-row:nth-child(odd){
|
||||
background: var(--green);
|
||||
}
|
||||
.arrivee-row:nth-child(even){
|
||||
background: var(--green-dark);
|
||||
}
|
||||
|
||||
/* Cacher la dernière ligne qui sera sous le footer */
|
||||
/* ne plus cacher la dernière ligne — on laisse le footer la recouvrir visuellement */
|
||||
|
||||
.col-logo-arrivees{
|
||||
max-width: 100px;
|
||||
display:flex;
|
||||
/* organiser verticalement les éléments et les centrer verticalement */
|
||||
flex-direction: column;
|
||||
justify-content: center; /* centre verticalement le contenu (logo + meta) */
|
||||
align-items: flex-start; /* garder l'alignement horizontal à gauche */
|
||||
flex: 0 0 10%; /* largeur fixe pour se comporter comme une colonne de tableau */
|
||||
}
|
||||
.logo-box-arrivees{
|
||||
/* pastille/logo : ne pas forcer la hauteur pour permettre l'empilement vertical */
|
||||
height: auto;
|
||||
width: 100px;
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:center;
|
||||
font-weight:800;
|
||||
font-size: clamp(14px, 1.6vw, 20px);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start; /* centrer la pastille dans la colonne */
|
||||
}
|
||||
|
||||
/* Logo SVG chargé depuis public/type */
|
||||
.op-logo-arrivees{
|
||||
max-width: 90%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Texte à l'intérieur de la pastille/logo (quai) */
|
||||
.logo-text-arrivees{
|
||||
text-align: center;
|
||||
/* taille plus grande, responsive ; s'assure qu'elle reste lisible sur petits et grands écrans */
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
|
||||
.col-time-arrivees{
|
||||
max-width: 15%;
|
||||
padding-left: 20px;
|
||||
display:flex;
|
||||
/* colonne verticale : centrer verticalement l'heure */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 18%; /* largeur fixe pour alignement table-like */
|
||||
}
|
||||
.time-arrivees{
|
||||
color: var(--accent-yellow);
|
||||
font-weight:850;
|
||||
/* tailles responsives similaires à l'image */
|
||||
font-size: clamp(26px, 7.5vw, 70px);
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: baseline; /* centre le chiffre verticalement */
|
||||
height: auto;
|
||||
width: 100%; /* occupe toute la colonne pour être centré */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-destination-arrivees{
|
||||
flex: 1 1 auto; /* colonne flexible qui prend l'espace restant */
|
||||
display: flex;
|
||||
/* colonne verticale : centrer verticalement la destination */
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start; /* garder le texte aligné à gauche */
|
||||
}
|
||||
.destination-arrivees{
|
||||
color: var(--white);
|
||||
font-weight: 500;
|
||||
font-size: clamp(28px, 8vw, 80px);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
background: var(--blue-bg);
|
||||
font-family: "Achemine", "Arial Narrow", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.col-platform-arrivees{
|
||||
/* La colonne laisse la boîte gérer sa taille ; on aligne la boîte à droite
|
||||
et l'on centre verticalement pour qu'elle suive la hauteur de la ligne. */
|
||||
display:flex;
|
||||
flex: 0 0 8%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0.5vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.platform-box-arrivees{
|
||||
/* Boîte quai : garder un format carré mais ne pas forcer l'étirement vertical */
|
||||
height: 90px; /* limite maximale, garde l'aspect visuel */
|
||||
width: 90px;
|
||||
aspect-ratio: 1 / 1; /* carré strict */
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background: transparent; /* intérieur transparent pour montrer la couleur de la ligne */
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
border: 4px solid #ffffff; /* bord blanc épais */
|
||||
padding: 8px; /* espace intérieur pour éloigner le texte du bord */
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
.platform-text-arrivees{
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
/* réduire légèrement la taille du texte afin qu'il ne touche pas la bordure */
|
||||
font-size: clamp(60px, 2.6vw, 100px);
|
||||
line-height:1;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
/* removed duplicated/corrupted block */
|
||||
|
||||
.col-train-type-arrivees{
|
||||
max-width: 9%;
|
||||
display:flex;
|
||||
/* colonne verticale : centrer verticalement l'heure */
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
flex: 0 0 18%; /* largeur fixe pour alignement table-like */
|
||||
}
|
||||
.train-type-box-arrivees{
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.train-type-fallback-arrivees{ text-align:center; }
|
||||
|
||||
|
||||
.train-meta-arrivees{
|
||||
flex-direction:column;
|
||||
align-items:flex-start;
|
||||
/* centrer verticalement sans forcer la hauteur (pour permettre l'empilement) */
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.train-meta-arrivees .op-name-arrivees{
|
||||
font-weight:900;
|
||||
font-size:1.50rem; /* plus visible */
|
||||
padding-left: 20px;
|
||||
line-height:1;
|
||||
justify-content: flex-start;
|
||||
text-align: left; /* aligner à gauche dans la colonne */
|
||||
}
|
||||
.train-meta-arrivees .op-number-arrivees{
|
||||
font-weight:500;
|
||||
font-size:1.25rem;
|
||||
padding-left: 20px;
|
||||
text-align: left; /* aligner à gauche */
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
|
||||
.train-meta-arrivees.status-delayed-arrivees .op-name-arrivees,
|
||||
.train-meta-arrivees.status-delayed-arrivees .op-number-arrivees,
|
||||
.train-meta-arrivees.status-suppressed-arrivees .op-name-arrivees,
|
||||
.train-meta-arrivees.status-suppressed-arrivees .op-number-arrivees{
|
||||
color: var(--accent-yellow);
|
||||
}
|
||||
|
||||
/* Si on veut un alignement plus spécifique quand train-meta apparaît dans la colonne de type */
|
||||
.col-train-type-arrivees .train-meta-arrivees{ align-items: flex-start; justify-content: center; }
|
||||
.col-train-type-arrivees .train-meta-arrivees .op-name-arrivees,
|
||||
.col-train-type-arrivees .train-meta-arrivees .op-number-arrivees{ text-align: left; }
|
||||
|
||||
.watermark-arrivees{
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
WATERMARK "arrivées" — pivotée à 90°, fixée à droite
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__watermark {
|
||||
position: absolute;
|
||||
/* placer strictement à droite et faire déborder le watermark en dehors de l'afficheur */
|
||||
right: -6vw; /* décalage vers l'extérieur pour qu'il soit strictement à droite */
|
||||
right: -9vh;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: center;
|
||||
font-size: 10.8vw;
|
||||
color: var(--watermark-green);
|
||||
font-weight:800;
|
||||
letter-spacing: 6px;
|
||||
/* Taille géante, très visible comme dans l'image */
|
||||
font-size: 23vh;
|
||||
color: #216b3e;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.05em;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
/* s'assurer qu'il soit au-dessus des lignes */
|
||||
z-index: 45;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
opacity: 0.3; /* Opacité diminuée */
|
||||
}
|
||||
|
||||
.helpline-arrivees{
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 12px;
|
||||
width:80px;
|
||||
height:80px;
|
||||
border-radius:6px;
|
||||
border:3px solid #fff;
|
||||
color:#fff;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-weight:900;
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
LISTE STATIQUE — 10 lignes sur exactement 100vh
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.clock-container-arrivees{
|
||||
position: relative; /* contenu positionné par le footer */
|
||||
pointer-events: none;
|
||||
z-index: 30; /* au-dessus du footer */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%; /* prendra la hauteur de la ligne footer */
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
MODE DÉFILEMENT — conteneur + track animé
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__scroll-container {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.clock-box-arrivees{
|
||||
display: inline-flex;
|
||||
|
||||
/* Le track contient 2× les données pour une boucle seamless */
|
||||
.aff-classiques-arrivees__scroll-track {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
ROW — chaque ligne occupe 1/10 de 100vh
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--green-dark); /* fond foncé interne comme sur la maquette */
|
||||
border-radius: 10px;
|
||||
padding: 4px 8px; /* réduire padding pour diminuer légèrement la taille globale */
|
||||
border: 6px solid #ffffff; /* bordure un peu moins épaisse */
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,0.12);
|
||||
flex: 0 0 calc(100vh / 10);
|
||||
height: calc(100vh / 10);
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* hauteur adaptée pour rentrer exactement dans la ligne footer */
|
||||
height: calc(var(--footer-height) - 12px);
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.afficheur-footer-arrivees{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: var(--footer-height);
|
||||
/* Alternance de tons bleus (comme l'image) */
|
||||
.aff-classiques-arrivees__row:nth-child(odd) { background: var(--blue-odd); }
|
||||
.aff-classiques-arrivees__row:nth-child(even) { background: var(--blue-even); }
|
||||
|
||||
.aff-classiques-arrivees__row--empty {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-white);
|
||||
font-size: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 1 — Logo opérateur (~9 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-logo {
|
||||
width: 9vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 20; /* en dessous de l'horloge */
|
||||
}
|
||||
.afficheur-footer-arrivees .footer-inner-arrivees{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 0 0.4vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__logo-box {
|
||||
width: 100%;
|
||||
/* Logo prend ~65% de la hauteur de la ligne */
|
||||
height: 6vh;
|
||||
display: flex;
|
||||
align-items: center; /* centre verticalement le contenu (la clock) */
|
||||
justify-content: flex-end; /* place la clock à droite */
|
||||
padding-right: 12px; /* espace entre le bord et la clock pour le coin arrondi */
|
||||
/* léger dégradé pour donner l'effet de bande plus claire en haut */
|
||||
background: linear-gradient(to bottom, rgba(11,123,180,0.18) 0%, var(--green-dark) 30%);
|
||||
box-shadow: inset 0 6px 0 rgba(0,0,0,0.08);
|
||||
/* arrondir côté droit comme sur la maquette */
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* légère ligne (accent) en haut du footer pour séparer visuellement */
|
||||
.afficheur-footer-arrivees .footer-inner-arrivees::before{
|
||||
content: '';
|
||||
.aff-classiques-arrivees__op-logo {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__logo-text {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 2vh;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 2 — Type + Numéro OU Statut (~11 % largeur)
|
||||
Alterne toutes les 4 secondes
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-meta {
|
||||
width: 11vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5vw 0 0;
|
||||
box-sizing: border-box;
|
||||
gap: 0.05vh;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Vue normale — type (ex: "MOBIGO") */
|
||||
.aff-classiques-arrivees__train-type {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Vue normale — numéro de sillon (ex: "894251") */
|
||||
.aff-classiques-arrivees__train-number {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — à l'heure */
|
||||
.aff-classiques-arrivees__status-ontime {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — retardé */
|
||||
.aff-classiques-arrivees__status-delayed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-label {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-value {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — supprimé */
|
||||
.aff-classiques-arrivees__status-suppressed {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 3 — Heure (~15 % largeur)
|
||||
Grande, jaune vif (#FFCC00), bold — comme dans l'image
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-time {
|
||||
width: 15vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
padding: 0 0.3vw 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__time {
|
||||
/* Jaune vif exactement comme dans la vidéo/image */
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
/* Grande taille — occupe ~65% de la hauteur de ligne */
|
||||
font-size: 6.5vh;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 4 — Destination + Info cause (flex:1 — prend tout le reste)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-origin {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
padding-right: 0.5vw;
|
||||
}
|
||||
|
||||
/* Texte destination — blanc, bold, très grand */
|
||||
.aff-classiques-arrivees__destination {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 6.5vh;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ─── Wrapper animé cause perturbation ────────────────────────── */
|
||||
.aff-classiques-arrivees__info-wrapper {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
max-width: 0;
|
||||
animation: infoWrapperGrow 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__info-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform: translateX(100%);
|
||||
animation: infoSlideIn 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__separator {
|
||||
color: var(--text-white);
|
||||
font-size: 4.5vh;
|
||||
font-weight: 200;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 0 0.9vw;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__info-text {
|
||||
color: var(--text-white);
|
||||
font-size: 1.9vh;
|
||||
font-weight: 400;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
line-height: 1.25;
|
||||
max-width: 22vw;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 5 — Quai OU icône bus (~6.5 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__col-platform {
|
||||
width: 6.5vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5vh 0.4vw;
|
||||
}
|
||||
|
||||
/* Boîte quai — bordure blanche */
|
||||
.aff-classiques-arrivees__platform-box {
|
||||
width: 6vh;
|
||||
height: 6vh;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__platform-text {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 4.5vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
HORLOGE — superposée en bas à droite (z-index élevé)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-arrivees__footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
right: 1.2vw;
|
||||
bottom: 0.8vh;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
.clock-main-arrivees{
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
/* légèrement plus petit pour tenir dans le footer */
|
||||
font-size: clamp(28px, 4.2vw, 60px);
|
||||
line-height: 1;
|
||||
font-family: "Achemine", "Helvetica Neue", Arial, sans-serif;
|
||||
margin-right: 6px;
|
||||
|
||||
.aff-classiques-arrivees__clock-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clock-seconds-arrivees{
|
||||
color: var(--accent-yellow);
|
||||
|
||||
.aff-classiques-arrivees__clock-box {
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
background: rgba(0, 51, 102, 0.93);
|
||||
border-radius: 8px;
|
||||
padding: 0.4vh 1.1vw;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5vw;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__clock-main {
|
||||
color: var(--text-white);
|
||||
font-weight: 800;
|
||||
font-size: clamp(16px, 2.8vw, 36px);
|
||||
font-size: 4.4vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Ajustements pour petits écrans */
|
||||
@media (max-width: 700px){
|
||||
.col-logo-arrivees{width: 18%;}
|
||||
.col-time-arrivees{width: 24%;}
|
||||
.watermark-arrivees{display:none}
|
||||
.aff-classiques-arrivees__clock-seconds {
|
||||
color: var(--yellow);
|
||||
font-weight: 800;
|
||||
font-size: 2.7vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* masqué */
|
||||
.aff-classiques-arrivees__station-box { display: none; }
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,15 +1,6 @@
|
||||
|
||||
.aff-classiques-departs__container{
|
||||
--blue-dark: #173771; /* bande foncée */
|
||||
--blue: #275e9f; /* bande claire */
|
||||
--accent-yellow: #e1c14c; /* heure */
|
||||
--white: #f3f3f3;
|
||||
--rows: 9; /* 9 horaires visibles + 1 footer */
|
||||
--footer-height: 100px; /* hauteur de la dernière 'ligne' servant de footer */
|
||||
--watermark-blue: rgba(10,120,180,0.15);
|
||||
}
|
||||
|
||||
/* Déclaration des fontes Achemine (dans public/fonts/) */
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
Fontes Achemine
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
@@ -32,367 +23,400 @@
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Conteneur plein écran pour l'afficheur */
|
||||
.aff-classiques-departs__container{
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
ANIMATIONS
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Info cause perturbation — slide-in de droite vers la gauche */
|
||||
@keyframes infoWrapperGrow {
|
||||
from { max-width: 0; }
|
||||
to { max-width: 30vw; }
|
||||
}
|
||||
|
||||
@keyframes infoSlideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
40% { opacity: 1; }
|
||||
to { transform: translateX(0%); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
CONTENEUR PRINCIPAL
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__container {
|
||||
/* Couleurs inspirées du vrai afficheur SNCF */
|
||||
--yellow: #FFCC00;
|
||||
--blue-bg: #003a79;
|
||||
--blue-odd: #0163ab;
|
||||
--blue-even: #003a79;
|
||||
--text-white: #ffffff;
|
||||
--text-muted: #7EB8E0;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
/* autoriser l'affichage des boîtes agrandies qui débordent des lignes */
|
||||
overflow: visible;
|
||||
font-family: "Achemine", "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--blue-dark);
|
||||
/* plus de padding-bottom : le footer sera superposé en position absolute */
|
||||
}
|
||||
|
||||
/* Boîte affichant le nom de la gare configuré via ?gare=Nom-de-la-gare */
|
||||
.aff-classiques-departs__station-box{
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
top: 12px;
|
||||
color: #ffffff;
|
||||
background: rgba(0,0,0,0.08);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
/* arrondir légèrement le bas de l'afficheur pour coller à la maquette */
|
||||
.aff-classiques-departs__container{
|
||||
border-bottom-right-radius: 10px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
/* Liste des lignes (bandes horizontales) */
|
||||
.aff-classiques-departs__list{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* La liste occupe toute la hauteur de l'écran; le footer sera affiché
|
||||
en position absolue par-dessus la partie inférieure. */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row{
|
||||
display: flex;
|
||||
/* laisser les colonnes occuper toute la hauteur de la ligne, puis centrer leur contenu */
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
padding: 0 1.2vw;
|
||||
/* Répartit l'espace en var(--rows) parts ; on utilise 100% / var(--rows) pour que
|
||||
9 lignes + 1 footer occupent l'écran de façon uniforme. */
|
||||
flex: 0 0 calc(100% / var(--rows));
|
||||
min-height: 50px; /* garde une bonne hauteur sur petits écrans */
|
||||
max-height: 120px;
|
||||
position: relative; /* pour positionner la .platform-box en absolu */
|
||||
overflow: visible; /* laisser la platform-box déborder si agrandie */
|
||||
}
|
||||
|
||||
.aff-classiques-departs__row:nth-child(odd){
|
||||
background: var(--blue);
|
||||
}
|
||||
.aff-classiques-departs__row:nth-child(even){
|
||||
background: var(--blue-dark);
|
||||
}
|
||||
|
||||
/* Colonne logo */
|
||||
.aff-classiques-departs__col-logo{
|
||||
max-width: 100px;
|
||||
display:flex;
|
||||
/* organiser verticalement les éléments et les centrer verticalement */
|
||||
flex-direction: column;
|
||||
justify-content: center; /* centre verticalement le contenu (logo + meta) */
|
||||
align-items: flex-start; /* garder l'alignement horizontal à gauche */
|
||||
flex: 0 0 10%; /* largeur fixe pour se comporter comme une colonne de tableau */
|
||||
}
|
||||
.aff-classiques-departs__logo-box{
|
||||
/* pastille/logo : ne pas forcer la hauteur pour permettre l'empilement vertical */
|
||||
height: auto;
|
||||
width: 100px;
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
justify-content:center;
|
||||
font-weight:800;
|
||||
font-size: clamp(14px, 1.6vw, 20px);
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start; /* centrer la pastille dans la colonne */
|
||||
}
|
||||
|
||||
/* Logo SVG chargé depuis public/type */
|
||||
.aff-classiques-departs__op-logo{
|
||||
max-width: 90%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Texte à l'intérieur de la pastille/logo (quai) */
|
||||
.aff-classiques-departs__logo-text{
|
||||
text-align: center;
|
||||
/* taille plus grande, responsive ; s'assure qu'elle reste lisible sur petits et grands écrans */
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
|
||||
/* Colonne heure */
|
||||
.aff-classiques-departs__col-time{
|
||||
max-width: 15%;
|
||||
padding-left: 20px;
|
||||
display:flex;
|
||||
/* colonne verticale : centrer verticalement l'heure */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 18%; /* largeur fixe pour alignement table-like */
|
||||
}
|
||||
.aff-classiques-departs__time{
|
||||
color: var(--accent-yellow);
|
||||
font-weight:850;
|
||||
/* tailles responsives similaires à l'image */
|
||||
font-size: clamp(26px, 7.5vw, 70px);
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: baseline; /* centre le chiffre verticalement */
|
||||
height: auto;
|
||||
width: 100%; /* occupe toute la colonne pour être centré */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Destination */
|
||||
.aff-classiques-departs__col-destination{
|
||||
flex: 1 1 auto; /* colonne flexible qui prend l'espace restant */
|
||||
display: flex;
|
||||
/* colonne verticale : centrer verticalement la destination */
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start; /* garder le texte aligné à gauche */
|
||||
}
|
||||
.aff-classiques-departs__destination{
|
||||
color: var(--white);
|
||||
font-weight: 500;
|
||||
font-size: clamp(28px, 8vw, 80px);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
background: var(--blue-bg);
|
||||
font-family: "Achemine", "Arial Narrow", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Colonne Platform (quai) affichée à droite de chaque ligne */
|
||||
.aff-classiques-departs__col-platform{
|
||||
/* La colonne laisse la boîte gérer sa taille ; on aligne la boîte à droite
|
||||
et l'on centre verticalement pour qu'elle suive la hauteur de la ligne. */
|
||||
display:flex;
|
||||
flex: 0 0 8%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0.5vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.aff-classiques-departs__platform-box{
|
||||
/* Boîte quai : garder un format carré mais ne pas forcer l'étirement vertical */
|
||||
height: 90px; /* limite maximale, garde l'aspect visuel */
|
||||
width: 90px;
|
||||
aspect-ratio: 1 / 1; /* carré strict */
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background: transparent; /* intérieur transparent pour montrer la couleur de la ligne */
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
border: 4px solid #ffffff; /* bord blanc épais */
|
||||
padding: 8px; /* espace intérieur pour éloigner le texte du bord */
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
.aff-classiques-departs__platform-text{
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
/* réduire légèrement la taille du texte afin qu'il ne touche pas la bordure */
|
||||
font-size: clamp(60px, 2.6vw, 100px);
|
||||
line-height:1;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
/* Colonne Type de train (victoire de la séparation avec la colonne logo) */
|
||||
.aff-classiques-departs__col-train-type{
|
||||
max-width: 9%;
|
||||
display:flex;
|
||||
/* colonne verticale : centrer verticalement l'heure */
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
flex: 0 0 18%; /* largeur fixe pour alignement table-like */
|
||||
}
|
||||
.aff-classiques-departs__train-type-box{
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.aff-classiques-departs__train-type-fallback{ text-align:center; }
|
||||
|
||||
|
||||
/* Numéro de train / opérateur (texte sous logo) */
|
||||
.aff-classiques-departs__train-meta{
|
||||
flex-direction:column;
|
||||
align-items:flex-start;
|
||||
/* centrer verticalement sans forcer la hauteur (pour permettre l'empilement) */
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.aff-classiques-departs__train-meta .aff-classiques-departs__op-name{
|
||||
font-weight:900;
|
||||
font-size:1.50rem; /* plus visible */
|
||||
padding-left: 20px;
|
||||
line-height:1;
|
||||
justify-content: flex-start;
|
||||
text-align: left; /* aligner à gauche dans la colonne */
|
||||
}
|
||||
.aff-classiques-departs__train-meta .aff-classiques-departs__op-number{
|
||||
font-weight:500;
|
||||
font-size:1.25rem;
|
||||
padding-left: 20px;
|
||||
text-align: left; /* aligner à gauche */
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
|
||||
/* Statuts spéciaux : retardé / supprimé doivent ressortir en jaune */
|
||||
.aff-classiques-departs__train-meta.aff-classiques-departs__train-meta--delayed .aff-classiques-departs__op-name,
|
||||
.aff-classiques-departs__train-meta.aff-classiques-departs__train-meta--delayed .aff-classiques-departs__op-number,
|
||||
.aff-classiques-departs__train-meta.aff-classiques-departs__train-meta--suppressed .aff-classiques-departs__op-name,
|
||||
.aff-classiques-departs__train-meta.aff-classiques-departs__train-meta--suppressed .aff-classiques-departs__op-number{
|
||||
color: var(--accent-yellow);
|
||||
}
|
||||
|
||||
/* Si on veut un alignement plus spécifique quand train-meta apparaît dans la colonne de type */
|
||||
.aff-classiques-departs__col-train-type .aff-classiques-departs__train-meta{ align-items: flex-start; justify-content: center; }
|
||||
.aff-classiques-departs__col-train-type .aff-classiques-departs__train-meta .aff-classiques-departs__op-name,
|
||||
.aff-classiques-departs__col-train-type .aff-classiques-departs__train-meta .aff-classiques-departs__op-number{ text-align: left; }
|
||||
|
||||
/* Watermark vertical à droite */
|
||||
.aff-classiques-departs__watermark{
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
WATERMARK "départs" — pivotée à 90°, fixée à droite
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__watermark {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
right: -9vh;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: center;
|
||||
font-size: 10.8vw;
|
||||
color: var(--watermark-blue);
|
||||
font-weight:800;
|
||||
letter-spacing: 6px;
|
||||
/* Taille géante, très visible comme dans l'image */
|
||||
font-size: 23vh;
|
||||
color: #1389D2;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.05em;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
opacity: 0.3; /* Opacité diminuée */
|
||||
}
|
||||
|
||||
/* Petite pastille H en haut à droite (optionnelle) */
|
||||
.aff-classiques-departs__helpline{
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 12px;
|
||||
width:80px;
|
||||
height:80px;
|
||||
border-radius:6px;
|
||||
border:3px solid #fff;
|
||||
color:#fff;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-weight:900;
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
LISTE STATIQUE — 10 lignes sur exactement 100vh
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Horloge en bas à droite, proche du bord droit (à 400px du bord droit) */
|
||||
.aff-classiques-departs__clock-container{
|
||||
position: relative; /* contenu positionné par le footer */
|
||||
pointer-events: none;
|
||||
z-index: 30; /* au-dessus du footer */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%; /* prendra la hauteur de la ligne footer */
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
MODE DÉFILEMENT — conteneur + track animé
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__scroll-container {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.aff-classiques-departs__clock-box{
|
||||
display: inline-flex;
|
||||
|
||||
/* Le track contient 2× les données pour une boucle seamless */
|
||||
.aff-classiques-departs__scroll-track {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
ROW — chaque ligne occupe 1/10 de 100vh
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--blue-dark); /* fond foncé interne comme sur la maquette */
|
||||
border-radius: 10px;
|
||||
padding: 4px 8px; /* réduire padding pour diminuer légèrement la taille globale */
|
||||
border: 6px solid #ffffff; /* bordure un peu moins épaisse */
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,0.12);
|
||||
flex: 0 0 calc(100vh / 10);
|
||||
height: calc(100vh / 10);
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* hauteur adaptée pour rentrer exactement dans la ligne footer */
|
||||
height: calc(var(--footer-height) - 12px);
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
/* Footer visuel : barre foncée en bas qui chevauche la liste */
|
||||
.aff-classiques-departs__footer{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: var(--footer-height);
|
||||
/* Alternance de tons bleus (comme l'image) */
|
||||
.aff-classiques-departs__row:nth-child(odd) { background: var(--blue-odd); }
|
||||
.aff-classiques-departs__row:nth-child(even) { background: var(--blue-even); }
|
||||
|
||||
.aff-classiques-departs__row--empty {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-white);
|
||||
font-size: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 1 — Logo opérateur (~9 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__col-logo {
|
||||
width: 9vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 20; /* en dessous de l'horloge */
|
||||
}
|
||||
.aff-classiques-departs__footer .aff-classiques-departs__footer-inner{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 0 0.4vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__logo-box {
|
||||
width: 100%;
|
||||
/* Logo prend ~65% de la hauteur de la ligne */
|
||||
height: 6vh;
|
||||
display: flex;
|
||||
align-items: center; /* centre verticalement le contenu (la clock) */
|
||||
justify-content: flex-end; /* place la clock à droite */
|
||||
padding-right: 12px; /* espace entre le bord et la clock pour le coin arrondi */
|
||||
/* léger dégradé pour donner l'effet de bande plus claire en haut */
|
||||
background: linear-gradient(to bottom, rgba(11,123,180,0.18) 0%, var(--blue-dark) 30%);
|
||||
box-shadow: inset 0 6px 0 rgba(0,0,0,0.08);
|
||||
/* arrondir côté droit comme sur la maquette */
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* légère ligne (accent) en haut du footer pour séparer visuellement */
|
||||
.aff-classiques-departs__footer .aff-classiques-departs__footer-inner::before{
|
||||
content: '';
|
||||
.aff-classiques-departs__op-logo {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__logo-text {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 2vh;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 2 — Type + Numéro OU Statut (~11 % largeur)
|
||||
Alterne toutes les 4 secondes
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__col-meta {
|
||||
width: 11vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5vw 0 0;
|
||||
box-sizing: border-box;
|
||||
gap: 0.05vh;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Vue normale — type (ex: "MOBIGO") */
|
||||
.aff-classiques-departs__train-type {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Vue normale — numéro de sillon (ex: "894251") */
|
||||
.aff-classiques-departs__train-number {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — à l'heure */
|
||||
.aff-classiques-departs__status-ontime {
|
||||
font-size: 2.2vh;
|
||||
color: var(--text-white);
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — retardé */
|
||||
.aff-classiques-departs__status-delayed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-label {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-value {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vue statut — supprimé */
|
||||
.aff-classiques-departs__status-suppressed {
|
||||
font-size: 2.2vh;
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 3 — Heure (~15 % largeur)
|
||||
Grande, jaune vif (#FFCC00), bold — comme dans l'image
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__col-time {
|
||||
width: 15vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
padding: 0 0.3vw 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__time {
|
||||
/* Jaune vif exactement comme dans la vidéo/image */
|
||||
color: var(--yellow);
|
||||
font-weight: 700;
|
||||
/* Grande taille — occupe ~65% de la hauteur de ligne */
|
||||
font-size: 6.5vh;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 4 — Destination + Info cause (flex:1 — prend tout le reste)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__col-destination {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
padding-right: 0.5vw;
|
||||
}
|
||||
|
||||
/* Texte destination — blanc, bold, très grand */
|
||||
.aff-classiques-departs__destination {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 6.5vh;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ─── Wrapper animé cause perturbation ────────────────────────── */
|
||||
.aff-classiques-departs__info-wrapper {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
max-width: 0;
|
||||
animation: infoWrapperGrow 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__info-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform: translateX(100%);
|
||||
animation: infoSlideIn 0.85s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__separator {
|
||||
color: var(--text-white);
|
||||
font-size: 4.5vh;
|
||||
font-weight: 200;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 0 0.9vw;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__info-text {
|
||||
color: var(--text-white);
|
||||
font-size: 1.9vh;
|
||||
font-weight: 400;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
line-height: 1.25;
|
||||
max-width: 22vw;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
COL 5 — Quai OU icône bus (~6.5 % largeur)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__col-platform {
|
||||
width: 6.5vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5vh 0.4vw;
|
||||
}
|
||||
|
||||
/* Boîte quai — bordure blanche */
|
||||
.aff-classiques-departs__platform-box {
|
||||
width: 6vh;
|
||||
height: 6vh;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__platform-text {
|
||||
color: var(--text-white);
|
||||
font-weight: 700;
|
||||
font-size: 4.5vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
HORLOGE — superposée en bas à droite (z-index élevé)
|
||||
══════════════════════════════════════════════════════════════════ */
|
||||
.aff-classiques-departs__footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
right: 1.2vw;
|
||||
bottom: 0.8vh;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
.aff-classiques-departs__clock-main{
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
/* légèrement plus petit pour tenir dans le footer */
|
||||
font-size: clamp(28px, 4.2vw, 60px);
|
||||
line-height: 1;
|
||||
font-family: "Achemine", "Helvetica Neue", Arial, sans-serif;
|
||||
margin-right: 6px;
|
||||
|
||||
.aff-classiques-departs__clock-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.aff-classiques-departs__clock-seconds{
|
||||
color: var(--accent-yellow);
|
||||
|
||||
.aff-classiques-departs__clock-box {
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.88);
|
||||
background: rgba(0, 51, 102, 0.93);
|
||||
border-radius: 8px;
|
||||
padding: 0.4vh 1.1vw;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5vw;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__clock-main {
|
||||
color: var(--text-white);
|
||||
font-weight: 800;
|
||||
font-size: clamp(16px, 2.8vw, 36px);
|
||||
font-size: 4.4vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Ajustements pour petits écrans */
|
||||
@media (max-width: 700px){
|
||||
.aff-classiques-departs__col-logo{width: 18%;}
|
||||
.aff-classiques-departs__col-time{width: 24%;}
|
||||
|
||||
.aff-classiques-departs__clock-seconds {
|
||||
color: var(--yellow);
|
||||
font-weight: 800;
|
||||
font-size: 2.7vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* masqué */
|
||||
.aff-classiques-departs__station-box { display: none; }
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
import Logo from '../../../components/Logo';
|
||||
import MarqueeText from '../../../components/MarqueeText';
|
||||
|
||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
@@ -22,46 +23,15 @@ const formatTimeNoSeconds = (v) => {
|
||||
return String(v).slice(0,5).replace(':','h');
|
||||
};
|
||||
|
||||
// Données proviennent exclusivement de la table `horaires`.
|
||||
function Logo({op}){
|
||||
// Tente de charger un logo SVG depuis /type/ en fonction de l'opérateur.
|
||||
// mapping pour opérateurs connus -> noms de fichiers dans public/type
|
||||
const mapping = {
|
||||
'TGV inOUI': 'logo-inoui.svg',
|
||||
'TGV inOUI ': 'logo-inoui.svg',
|
||||
'TGV': 'logo-tgv.svg',
|
||||
'TER': 'sncf-voyageurs-logo.png',
|
||||
'MOBIGO': 'logo-mobigo.svg',
|
||||
'TGV Lyria': 'logo-lyria.svg',
|
||||
'TGV Lyria ': 'logo-lyria.svg',
|
||||
};
|
||||
|
||||
const defaultFileFromOp = (name) => {
|
||||
if (!name) return null;
|
||||
// nettoyer et fabriquer un nom plausible : "logo-<slug>.svg"
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
return `logo-${slug}.svg`;
|
||||
};
|
||||
|
||||
const fileName = mapping[op] || mapping[op.split(' ')[0]] || defaultFileFromOp(op);
|
||||
|
||||
const [hasImg, setHasImg] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__logo-box" aria-hidden>
|
||||
{hasImg && fileName ? (
|
||||
<img
|
||||
src={`/type/${fileName}`}
|
||||
alt={op}
|
||||
className="aff-classiques-departs__op-logo"
|
||||
onError={()=> setHasImg(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__logo-text" aria-hidden>{op.split(' ')[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const isSubstitution = (h) => {
|
||||
if (!h) return false;
|
||||
if (h.substitution_disponible === true) return true;
|
||||
const subFlag = h.substitution === true || h.is_substitution === true || h.isSubstitution === true;
|
||||
const subStr = (typeof h.substitution === 'string' && ['true','1','yes','y','t'].includes(h.substitution.toLowerCase()));
|
||||
const typeMatch = h.type_train && /subst|substitut|remplac/i.test(String(h.type_train));
|
||||
const commentMatch = h.comment && /subst|substitut|remplac/i.test(String(h.comment));
|
||||
return Boolean(subFlag || subStr || typeMatch || commentMatch);
|
||||
};
|
||||
|
||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||
// informations via .train-meta dans la colonne .col-train-type)
|
||||
@@ -95,13 +65,17 @@ export default function ClassiquesDeparts(){
|
||||
const gareIdParam = params.get('gare_id') || params.get('gareId');
|
||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
|
||||
const defilementMode = params.get('defilement') === 'true';
|
||||
|
||||
// debug flag activable via ?debug=1 or ?debug=true
|
||||
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
||||
const debug = useCallback((...args) => { if (debugEnabled) console.debug(...args); }, [debugEnabled]);
|
||||
|
||||
// État pour alterner l'affichage toutes les 3 secondes
|
||||
// Alterner toutes les 2 secondes entre type+numéro et statut
|
||||
const [showStatus, setShowStatus] = useState(false);
|
||||
// Afficher la cause de perturbation 5s, cacher 5s (cycle indépendant)
|
||||
const [showInfoCause, setShowInfoCause] = useState(true);
|
||||
// État pour l'heure actuelle, mise à jour chaque seconde pour la logique d'affichage du quai
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
|
||||
@@ -123,6 +97,38 @@ export default function ClassiquesDeparts(){
|
||||
// debug des données brutes renvoyées par Supabase
|
||||
const [rawDebug, setRawDebug] = useState({ count: 0, sample: [] });
|
||||
|
||||
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const fetchStationType = async () => {
|
||||
try {
|
||||
let fetchedType = null;
|
||||
if (gareIdParam) {
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
||||
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
} else if (gareDisplay) {
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && 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;
|
||||
});
|
||||
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
if (supabase) {
|
||||
fetchStationType();
|
||||
}
|
||||
return () => { mounted = false; };
|
||||
}, [gareIdParam, gareDisplay]);
|
||||
|
||||
// --- Helpers pour perturbations (définis ici pour éviter use-before-define dans useEffect) ---
|
||||
const normalizeName = useCallback((s) => {
|
||||
if (!s) return '';
|
||||
@@ -252,12 +258,19 @@ export default function ClassiquesDeparts(){
|
||||
}, []);
|
||||
|
||||
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
|
||||
const thresholdForStationType = useCallback((type) => {
|
||||
if (!type) return 0;
|
||||
const t = String(type).trim().toLowerCase();
|
||||
if (t === 'interurbaine' || t.includes('interurb')) return 360; // 6 heures
|
||||
if (t === 'ville' || t.includes('ville')) return 30; // 30 minutes
|
||||
return 0;
|
||||
const thresholdForStationType = useCallback((typeGare) => {
|
||||
const tG = String(typeGare || '').trim().toLowerCase();
|
||||
|
||||
// Vérification du type de gare
|
||||
if (tG === 'interurbaine' || tG.includes('interurb')) {
|
||||
return 360; // 6 heures
|
||||
}
|
||||
if (tG === 'ville' || tG.includes('ville')) {
|
||||
return 30; // 30 minutes
|
||||
}
|
||||
|
||||
// par défaut, 30 minutes
|
||||
return 30;
|
||||
}, []);
|
||||
|
||||
const formatTimeFromHM = useCallback((totalMinutes) => {
|
||||
@@ -354,81 +367,172 @@ export default function ClassiquesDeparts(){
|
||||
}, [buildPerturbMap, normalizeName, debug]);
|
||||
|
||||
|
||||
// serviceDate (YYYY-MM-DD) optional: forces the evaluation date for perturbations when horaire._date is missing
|
||||
// serviceDate (YYYY-MM-DD) optional: forces the evaluation date for perturbations when horaire._date is missing
|
||||
const computeStatusForHoraireAtStop = useCallback((horaire, stationId, stationName, idx, serviceDate = null) => {
|
||||
const found = findPerturbsForHoraireAndStop(horaire, stationId, stationName, idx) || [];
|
||||
try { debug('computeStatusForHoraireAtStop', { id: horaire && horaire.id, numero: horaire && (horaire.numero_train || horaire.numero), stationId, stationName, found: found.length }); } catch (e) {}
|
||||
const map = idx || buildPerturbMap();
|
||||
const allPerturbs = [];
|
||||
const hId = horaire && (horaire.id || horaire.horaire_id || horaire.numero || horaire.numero_train);
|
||||
const number = horaire && (horaire.numero_train || horaire.number || horaire.numero || horaire.train_number);
|
||||
|
||||
if (map) {
|
||||
const addedIds = new Set();
|
||||
const addP = (p) => {
|
||||
if (p && !addedIds.has(p.id)) {
|
||||
addedIds.add(p.id);
|
||||
allPerturbs.push(p);
|
||||
}
|
||||
};
|
||||
if (hId != null && map.byHoraire && map.byHoraire[String(hId)]) {
|
||||
map.byHoraire[String(hId)].forEach(addP);
|
||||
}
|
||||
if (number != null && map.byNumber && map.byNumber[String(number)]) {
|
||||
map.byNumber[String(number)].forEach(addP);
|
||||
}
|
||||
if (stationId != null && map.byStopId && map.byStopId[String(stationId)]) {
|
||||
map.byStopId[String(stationId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (stationName && map.byStopName) {
|
||||
const key = normalizeName(stationName);
|
||||
if (map.byStopName[key]) map.byStopName[key].forEach(x => addP(x.perturbation));
|
||||
for (const existingKey of Object.keys(map.byStopName)) {
|
||||
if (existingKey.includes(key) || key.includes(existingKey)) {
|
||||
map.byStopName[existingKey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
if (Array.isArray(trainStops)) {
|
||||
trainStops.forEach(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && map.byStopId && map.byStopId[String(sId)]) {
|
||||
map.byStopId[String(sId)].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
if (sName && map.byStopName) {
|
||||
const skey = normalizeName(sName);
|
||||
if (map.byStopName[skey]) map.byStopName[skey].forEach(x => addP(x.perturbation));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (map.global) {
|
||||
map.global.forEach(addP);
|
||||
}
|
||||
}
|
||||
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
|
||||
let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
|
||||
if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); };
|
||||
let horaireYmd = toYMD(horaire && (horaire._date || horaire.date || horaire.date_service || horaire.service_date));
|
||||
if (!horaireYmd && serviceDate) horaireYmd = serviceDate;
|
||||
|
||||
let maxDelay = null;
|
||||
let maxDelay = null;
|
||||
let isSuppressed = false;
|
||||
let causeDetails = '';
|
||||
|
||||
for (const p of found) {
|
||||
// date filter: if perturbation carries explicit dates or jours_perturbation, check they match horaireYmd
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
const pDate = toYMD(p.date || p.date_service || p.start_date || p.date_debut);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== horaireYmd) { debug('perturbation date mismatch', p.id); continue; }
|
||||
if (db && df && !(horaireYmd >= db && horaireYmd <= df)) { debug('perturbation date-range mismatch', p.id); continue; }
|
||||
}
|
||||
} catch (e) { /* ignore date parse errors -> assume applies */ }
|
||||
for (const p of allPerturbs) {
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== horaireYmd) continue;
|
||||
if (db && df && !(horaireYmd >= db && horaireYmd <= df)) continue;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && horaireYmd && !joursSet.has(horaireYmd)) continue;
|
||||
} catch (e) {}
|
||||
|
||||
// also check jours_perturbation (may be inside perturbations_arrets wrapper)
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && horaireYmd) {
|
||||
if (!joursSet.has(horaireYmd)) { debug('perturbation jours mismatch', p.id); continue; }
|
||||
}
|
||||
} catch (e) {}
|
||||
const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
|
||||
const numberRef = p.number || p.train_number || p.numero || p.numero_train;
|
||||
const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
|
||||
const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
|
||||
|
||||
const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
|
||||
const arrets = parsedObj.arrets || [];
|
||||
|
||||
const parsedObj = parsePerturbArretsObject(p) || { arrets: [], jours: null };
|
||||
const arrets = parsedObj.arrets || [];
|
||||
let matched = false;
|
||||
for (const a of arrets) {
|
||||
const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
const na = normalizeName(aName || '');
|
||||
const ns = normalizeName(stationName || '');
|
||||
const nameMatches = stationName && (na === ns || na.includes(ns) || ns.includes(na));
|
||||
if ((stationId != null && String(aId) === String(stationId)) || nameMatches) {
|
||||
matched = true;
|
||||
const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
const trainStops = getStops(horaire);
|
||||
let trainStopMatch = false;
|
||||
|
||||
if (!matched) {
|
||||
// top-level perturbation that references the horaire/numero
|
||||
const ref = p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero;
|
||||
const numberRef = p.number || p.train_number || p.numero || p.numero_train;
|
||||
const appliesByRef = ref && (String(ref) === String(horaire.id) || String(ref) === String(horaire.numero_train) || String(ref) === String(horaire.numero));
|
||||
const appliesByNumber = numberRef && (String(numberRef) === String(horaire.numero_train) || String(numberRef) === String(horaire.numero) || String(numberRef) === String(horaire.number));
|
||||
const isGlobal = (!Array.isArray(arrets) || arrets.length === 0) && !ref && !numberRef;
|
||||
if (appliesByRef || appliesByNumber || isGlobal) {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchesTarget = (a) => {
|
||||
if (!stationName && stationId == null) return false;
|
||||
const aId = a.gare_id || a.gareId || a.id || a.station_id || a.stationId || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
if (stationId != null && String(aId) === String(stationId)) return true;
|
||||
if (stationName && aName) {
|
||||
const na = normalizeName(aName);
|
||||
const ns = normalizeName(stationName);
|
||||
return na === ns;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [findPerturbsForHoraireAndStop, parsePerturbArretsObject, getPerturbationJours, parseDelayMinutesFromArret, normalizeName, debug]);
|
||||
for (const a of arrets) {
|
||||
const aId = a.gare_id || a.gareId || a.id || a.uic || a.code;
|
||||
const aName = a.gare_nom || a.nom || a.name || a.station_name || a.station_nom;
|
||||
const na = normalizeName(aName || '');
|
||||
|
||||
const isTarget = matchesTarget(a);
|
||||
let isTrainStop = false;
|
||||
if (trainStops && Array.isArray(trainStops)) {
|
||||
isTrainStop = trainStops.some(s => {
|
||||
const sId = s.gare_id || s.gareId || s.id || s.code;
|
||||
const sName = s.gare_nom || s.nom || s.name || s.stop_name;
|
||||
if (sId != null && aId != null && String(sId) === String(aId)) return true;
|
||||
if (sName && aName) {
|
||||
const ns = normalizeName(sName);
|
||||
return ns === na;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const originName = normalizeName(horaire.gare_depart_nom || horaire.gare_depart || '');
|
||||
const destName = normalizeName(horaire.gare_arrivee_nom || horaire.gare_arrivee || '');
|
||||
if (originName === na || destName === na) isTrainStop = true;
|
||||
|
||||
if (isTrainStop) trainStopMatch = true;
|
||||
|
||||
if (isTarget) {
|
||||
const t = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = (arrets.length === 0) && !ref && !numberRef;
|
||||
const applies = appliesByRef || appliesByNumber || trainStopMatch || isGlobal;
|
||||
|
||||
if (applies) {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
if (t.includes('arret_supprime') || /suppressed|supprim/.test(t)) {
|
||||
isSuppressed = true;
|
||||
}
|
||||
|
||||
for (const a of arrets) {
|
||||
const tType = (a.type_perturbation || a.type || '').toString().toLowerCase();
|
||||
if (tType.includes('retard') || tType.includes('delay') || /retard/.test(tType)) {
|
||||
const d = parseDelayMinutesFromArret(a) || parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
}
|
||||
if (t.includes('retard') || t.includes('delay') || /retard/.test(t)) {
|
||||
const d = parseDelayMinutesFromArret(p) || 0;
|
||||
if (maxDelay == null || d > maxDelay) maxDelay = d;
|
||||
}
|
||||
if (p.cause_details) {
|
||||
causeDetails = p.cause_details;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
// Applique les perturbations à une liste d'horaires : calcule status par arrêt et met à jour l'heure affichée
|
||||
const applyPerturbationsToHoraires = useCallback((list = [], perturbList = perturbations, displayStationId = null, displayStationName = null, serviceDate = null) => {
|
||||
const applyPerturbationsToHoraires = useCallback((list = [], perturbList = [], displayStationId = null, displayStationName = null, serviceDate = null) => {
|
||||
const map = buildPerturbMap(perturbList || []);
|
||||
// fallback to global display station context
|
||||
const targetStationId = displayStationId || gareIdParam || null;
|
||||
@@ -449,22 +553,6 @@ export default function ClassiquesDeparts(){
|
||||
// compute display time using delay at the display station if any
|
||||
const displayStatus = computeStatusForHoraireAtStop(horaire, targetStationId, targetStationName, map, serviceDate);
|
||||
let displayTime = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
if (displayStatus && typeof displayStatus.delayMinutes === 'number' && !isNaN(displayStatus.delayMinutes)) {
|
||||
if (typeof horaire._depHM === 'number') {
|
||||
displayTime = formatTimeFromHM(horaire._depHM + displayStatus.delayMinutes);
|
||||
} else {
|
||||
const tryParse = (s) => {
|
||||
if (!s) return null;
|
||||
const m = String(s).match(/(\d{1,2}):(\d{2})/);
|
||||
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
|
||||
return null;
|
||||
};
|
||||
const base = tryParse(horaire._rawTime || horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
if (typeof base === 'number') displayTime = formatTimeFromHM(base + displayStatus.delayMinutes);
|
||||
}
|
||||
}
|
||||
horaire._stopsStatus = stopsStatus;
|
||||
// prefer platform from stop data (quai) if available
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
@@ -472,20 +560,36 @@ export default function ClassiquesDeparts(){
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.platform || stopForStation.quay)) || horaire.platform || '';
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, formatTimeFromHM, perturbations, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatus(s => !s), 3000);
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Cycle 5s visible / 5s caché pour la cause de perturbation
|
||||
useEffect(() => {
|
||||
let timeoutId;
|
||||
const scheduleCycle = (currentlyVisible) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowInfoCause(!currentlyVisible);
|
||||
scheduleCycle(!currentlyVisible);
|
||||
}, 5000);
|
||||
};
|
||||
setShowInfoCause(true);
|
||||
scheduleCycle(true);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -554,29 +658,6 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
// stocker la liste complète (non filtrée) pour associer horaire_id -> numero_train
|
||||
if (mounted) setAllHoraires(parsedHoraires || []);
|
||||
|
||||
// Récupérer le type de gare depuis la table `gares` si possible
|
||||
try {
|
||||
let fetchedType = null;
|
||||
if (gareIdParam) {
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').eq('id', gareIdParam).limit(1);
|
||||
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
} else if (gareDisplay) {
|
||||
const needle = `%${gareDisplay}%`;
|
||||
try {
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').or(`nom.ilike.${needle},name.ilike.${needle}`).limit(1);
|
||||
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
} catch (e) {
|
||||
try {
|
||||
const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', needle).limit(1);
|
||||
if (gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setStationType(fetchedType ? String(fetchedType).toLowerCase() : null);
|
||||
}
|
||||
} catch (e) { /* ignore errors reading gares table */ }
|
||||
// Filtrer par gare : garder uniquement les horaires qui DESSERVENT la gare ciblée
|
||||
// (soit comme gare_depart, soit comme un arrêt intermédiaire dans `stops` / `gares_desservies`)
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
@@ -586,11 +667,11 @@ export default function ClassiquesDeparts(){
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName && (destName === normalized || destName.includes(normalized) || normalized.includes(destName))) return false;
|
||||
if (destName === normalized) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName && (departName === normalized || departName.includes(normalized) || normalized.includes(departName))) return true;
|
||||
if (departName === normalized) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -607,7 +688,7 @@ export default function ClassiquesDeparts(){
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized || stopName.includes(normalized) || normalized.includes(stopName)) return true;
|
||||
if (stopName === normalized) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -820,23 +901,14 @@ export default function ClassiquesDeparts(){
|
||||
return false;
|
||||
};
|
||||
|
||||
const isSubstitution = (h) => {
|
||||
if (!h) return false;
|
||||
// nouvelle colonne spécifique
|
||||
if (h.substitution_disponible === true) return true;
|
||||
// anciennes variantes
|
||||
const subFlag = h.substitution === true || h.is_substitution === true || h.isSubstitution === true;
|
||||
const subStr = (typeof h.substitution === 'string' && ['true','1','yes','y','t'].includes(h.substitution.toLowerCase()));
|
||||
const typeMatch = h.type_train && /subst|substitut|remplac/i.test(String(h.type_train));
|
||||
const commentMatch = h.comment && /subst|substitut|remplac/i.test(String(h.comment));
|
||||
return Boolean(subFlag || subStr || typeMatch || commentMatch);
|
||||
};
|
||||
|
||||
|
||||
// compute linkage and travaux applicability per horaire
|
||||
const enriched = enrich.map(h => {
|
||||
let linked = false;
|
||||
let appliesToday = false;
|
||||
let appliesTomorrow = false;
|
||||
let linkedTravaux = null;
|
||||
for (const t of travauxList) {
|
||||
// Extraire identifiants de ligne (différentes variantes possibles)
|
||||
const tLine = t.ligne_id || t.ligne || t.line_id || t.line || t.ligneId || t.lineId;
|
||||
@@ -856,12 +928,19 @@ export default function ClassiquesDeparts(){
|
||||
}
|
||||
|
||||
const ref = t.horaire_id || t.horaire || t.horaireId || t.numero_train || t.train_number || t.numero;
|
||||
if (ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero))) linked = true;
|
||||
if (!appliesToday && travauxApplies(t,h,today)) appliesToday = true;
|
||||
if (!appliesTomorrow && travauxApplies(t,h,tomorrow)) appliesTomorrow = true;
|
||||
if (linked && appliesToday && appliesTomorrow) break;
|
||||
let matchRef = ref && (String(ref) === String(h.id) || String(ref) === String(h.numero_train) || String(ref) === String(h.numero));
|
||||
if (matchRef) linked = true;
|
||||
|
||||
const appT = travauxApplies(t,h,today);
|
||||
const appY = travauxApplies(t,h,tomorrow);
|
||||
if (appT) appliesToday = true;
|
||||
if (appY) appliesTomorrow = true;
|
||||
|
||||
if (matchRef || appT || appY) {
|
||||
linkedTravaux = t;
|
||||
}
|
||||
}
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow };
|
||||
return { ...h, _linkedToTravaux: linked, _travauxAppliesToday: appliesToday, _travauxAppliesTomorrow: appliesTomorrow, _linkedTravauxObj: linkedTravaux };
|
||||
});
|
||||
|
||||
// Build today and tomorrow candidate lists
|
||||
@@ -869,7 +948,7 @@ export default function ClassiquesDeparts(){
|
||||
if (h._date && h._date !== todayStr) return false;
|
||||
if (!circulatesOn(h, today)) return false;
|
||||
if (typeof h._depHM !== 'number') return false;
|
||||
if (h._depHM <= todayHM) return false;
|
||||
if (h._depHM < todayHM - 1) return false;
|
||||
|
||||
// if travaux apply at this horaire today, hide regular horaires
|
||||
if (h._travauxAppliesToday && !isSubstitution(h)) return false;
|
||||
@@ -910,21 +989,21 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
// Construire un index local des perturbations à partir des données fraîchement récupérées
|
||||
// Filtrer pour ne garder que les perturbations applicables aujourd'hui (éviter d'appliquer les perturbations d'hier)
|
||||
const todayYmd = new Date().toLocaleDateString('en-CA');
|
||||
|
||||
const filteredPerturb = (perturbData || []).filter(p => {
|
||||
try {
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
|
||||
const pDate = toYMD(p.date || p.date_service || p.start_date || p.date_debut);
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
// si pDate fourni et ne correspond pas à aujourd'hui -> ignorer
|
||||
if (pDate && pDate !== todayYmd) return false;
|
||||
// si fourche de dates fournie et aujourd'hui n'est pas dans l'intervalle -> ignorer
|
||||
if (db && df && !(todayYmd >= db && todayYmd <= df)) return false;
|
||||
// si jours_perturbation fourni -> vérifier qu'aujourd'hui en fait partie
|
||||
// si pDate fourni et ne correspond pas à la date de service -> ignorer
|
||||
if (pDate && pDate !== serviceDateStr) return false;
|
||||
// si fourche de dates fournie et la date de service n'est pas dans l'intervalle -> ignorer
|
||||
if (db && df && !(serviceDateStr >= db && serviceDateStr <= df)) return false;
|
||||
// si jours_perturbation fourni -> vérifier que la date de service en fait partie
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet && !joursSet.has(todayYmd)) return false;
|
||||
if (joursSet && !joursSet.has(serviceDateStr)) return false;
|
||||
} catch (e) {
|
||||
// ignore parse errors and keep perturbation
|
||||
}
|
||||
@@ -952,8 +1031,8 @@ export default function ClassiquesDeparts(){
|
||||
// Debug: log first few annotated horaires
|
||||
try { debug('annotated horaires sample', annotated.slice(0,6).map(a => ({ id: a.id, numero: a.numero_train || a.numero, _status: a._status, _displayTime: a._displayTime }))); } catch (e) {}
|
||||
|
||||
// afficher seulement les 9 premières lignes sur l'afficheur
|
||||
setHoraires((annotated || []).slice(0, 9));
|
||||
// 10 lignes en mode normal, 20 en mode défilement
|
||||
setHoraires((annotated || []).slice(0, defilementMode ? 20 : 10));
|
||||
setPerturbations(filteredPerturb || []);
|
||||
setTravaux(travauxData || []);
|
||||
} catch (err) {
|
||||
@@ -972,124 +1051,223 @@ export default function ClassiquesDeparts(){
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
if (defilementMode && horaires.length > 0) {
|
||||
// Nombre de lignes de données
|
||||
const dataRows = horaires.length;
|
||||
// 10 lignes vides
|
||||
const emptyRowsCount = 10;
|
||||
|
||||
// Temps pour défiler les données (2s par ligne au lieu de 1s)
|
||||
const dataScrollTime = dataRows * 2;
|
||||
// Temps pour défiler l'espace vide (2s par ligne)
|
||||
const emptyScrollTime = emptyRowsCount * 2;
|
||||
// Temps de pause de 10 secondes
|
||||
const pauseTime = 10;
|
||||
|
||||
// Durée totale d'un cycle (défilement + pause)
|
||||
const totalAnimationTime = dataScrollTime + emptyScrollTime + pauseTime;
|
||||
|
||||
// Calcul des pourcentages pour l'animation
|
||||
// On commence à 0
|
||||
// À la fin du défilement des données, on a défilé de (dataRows / (totalCycleRows * 2)) * 100% de la hauteur totale
|
||||
// À la fin du défilement de l'espace, on a défilé de (totalCycleRows / (totalCycleRows * 2)) * 100% (soit 50%)
|
||||
// Ensuite on fait une pause pendant 10 secondes (donc le % de temps avance, mais pas la translation)
|
||||
|
||||
const scrollEndPercent = ((dataScrollTime + emptyScrollTime) / totalAnimationTime) * 100;
|
||||
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.innerText = `
|
||||
@keyframes customScrollMarquee {
|
||||
0% { transform: translateY(0); }
|
||||
${scrollEndPercent}% { transform: translateY(-50%); }
|
||||
100% { transform: translateY(-50%); }
|
||||
}
|
||||
.aff-classiques-departs__scroll-track {
|
||||
animation: customScrollMarquee ${totalAnimationTime}s linear infinite !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(styleSheet);
|
||||
};
|
||||
}
|
||||
}, [defilementMode, horaires]);
|
||||
|
||||
|
||||
const renderRow = (h, idx) => {
|
||||
if (h._isEmpty) {
|
||||
return <li className="aff-classiques-departs__row" key={`empty-${idx}`} style={{ background: 'transparent' }}></li>;
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const op = horaire.type_train || horaire.op || '';
|
||||
const isCarTER = (op === 'Car TER' || op === 'Autocar');
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
const platform = (horaire._displayPlatform || '').toString().trim();
|
||||
const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null);
|
||||
const stationName = gareDisplay || horaire.gare_depart_nom || horaire.gare_depart || horaire.origin_name || null;
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
|
||||
let showPlatform = !isSuppressed && Boolean(platform);
|
||||
if (showPlatform) {
|
||||
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
|
||||
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
if (typeof sched === 'number') {
|
||||
let diff = sched - nowHM;
|
||||
// Gérer le passage à minuit
|
||||
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
|
||||
diff += 1440; // treat as next day
|
||||
}
|
||||
showPlatform = (diff >= 0 && diff <= threshold);
|
||||
} else {
|
||||
showPlatform = false;
|
||||
}
|
||||
}
|
||||
|
||||
const isSub = isSubstitution(horaire);
|
||||
const infoText = isSub
|
||||
? (horaire.motif_substitution && horaire.motif_substitution !== 'TRAVAUX' && horaire.motif_substitution !== 'Travaux.' ? horaire.motif_substitution : 'Autocar cour de la gare Plateforme 14')
|
||||
: (status && status.causeDetails ? status.causeDetails : '');
|
||||
const trainNumber = horaire.numero_train || horaire.numero || horaire.number || '';
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-departs__row" key={idx}>
|
||||
|
||||
{/* COL 1 : Logo opérateur */}
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
{isCarTER ? (
|
||||
<div className="aff-classiques-departs__logo-box">
|
||||
<img src="/type/sncf-voyageurs-logo.png" alt="SNCF Voyageurs" className="aff-classiques-departs__op-logo" />
|
||||
</div>
|
||||
) : (
|
||||
<Logo
|
||||
op={op}
|
||||
classNameBox="aff-classiques-departs__logo-box"
|
||||
classNameImg="aff-classiques-departs__op-logo"
|
||||
classNameText="aff-classiques-departs__logo-text"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 2 : Type + Numéro (4s) OU Statut (4s) */}
|
||||
<div className="aff-classiques-departs__col-meta">
|
||||
{showStatus ? (
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">
|
||||
<div className="aff-classiques-departs__status-label">retard</div>
|
||||
<div className="aff-classiques-departs__status-value">{status.delayMinutes} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="aff-classiques-departs__train-type">{op}</div>
|
||||
<div className="aff-classiques-departs__train-number">{trainNumber}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Destination + Info cause (slide-in 5s/5s) */}
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={destination} className="aff-classiques-departs__destination" />
|
||||
{showInfoCause && infoText ? (
|
||||
<div className="aff-classiques-departs__info-wrapper">
|
||||
<div className="aff-classiques-departs__info-inner">
|
||||
<div className="aff-classiques-departs__separator">|</div>
|
||||
<div className="aff-classiques-departs__info-text">{infoText}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* COL 5 : Quai OU icône bus */}
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{isSub || isCarTER ? (
|
||||
<img src="/type/track-car.svg" alt="Car" style={{ width: '6.5vh', height: '6.5vh' }} />
|
||||
) : showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
<div className="aff-classiques-departs__platform-text">{platform}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="aff-classiques-departs__container">
|
||||
{/* nom de la gare configurable via ?gare=Nom-de-la-gare */}
|
||||
|
||||
{/* Watermark "départs" — diagonal droite */}
|
||||
<div className="aff-classiques-departs__watermark">départs</div>
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{horaires.length === 0 ? (
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key="empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
|
||||
{defilementMode ? (
|
||||
/* ── MODE DÉFILEMENT : marquee vertical infini ── */
|
||||
<div className="aff-classiques-departs__scroll-container">
|
||||
{horaires.length === 0 ? (
|
||||
<ul className="aff-classiques-departs__list">
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul
|
||||
className="aff-classiques-departs__scroll-track"
|
||||
>
|
||||
{rowsToRender.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* ── MODE NORMAL : 10 lignes statiques ── */
|
||||
<ul className="aff-classiques-departs__list">
|
||||
{horaires.length === 0 ? (
|
||||
<li className="aff-classiques-departs__row aff-classiques-departs__row--empty" key="empty">
|
||||
<div>Aucun horaire disponible.</div>
|
||||
<div style={{fontSize:'0.8rem', opacity:0.8}}>lignes brutes renvoyées : {rawDebug.count}</div>
|
||||
{rawDebug.count > 0 && rawDebug.sample && rawDebug.sample.length > 0 ? (
|
||||
<pre style={{maxHeight: 160, overflow: 'auto', fontSize: '0.75rem', marginTop: 8, background: '#111', color:'#fff', padding:8}}>
|
||||
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
|
||||
</pre>
|
||||
) : null}
|
||||
</li>
|
||||
) : horaires.map((h, idx) => {
|
||||
const horaire = h || {};
|
||||
const op = horaire.type_train || horaire.op || '';
|
||||
const number = horaire.numero_train || horaire.number || horaire.numero || '';
|
||||
// Use enriched display time (computed during fetch) — fallback to formatting raw fields
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
const platform = (horaire._displayPlatform || horaire.platform || '').toString().trim();
|
||||
// attachedPerturbs/rowObj previously used for mocked status; status is now computed from perturbations per stop
|
||||
|
||||
// determine station context (the display station) — prefer explicit gareIdParam/gareDisplay if provided
|
||||
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);
|
||||
|
||||
// prefer precomputed status and display time (synchronisé depuis la BDD) — sinon calculer à la volée
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
|
||||
if (status && status.label === 'supprimé' && showStatus) {
|
||||
time = '';
|
||||
}
|
||||
// if the status is 'supprimé' and the component is currently showing the status (showStatus toggles every 3s), hide the time
|
||||
if (status && status.label === 'supprimé' && showStatus) {
|
||||
time = '';
|
||||
} else if (!horaire._displayTime && status && typeof status.delayMinutes === 'number' && !isNaN(status.delayMinutes)) {
|
||||
// if we have _depHM (minutes since midnight) use it
|
||||
if (typeof horaire._depHM === 'number') {
|
||||
time = formatTimeFromHM(horaire._depHM + status.delayMinutes);
|
||||
} else {
|
||||
// fallback: try to parse original time string
|
||||
const tryParse = (s) => {
|
||||
if (!s) return null;
|
||||
const m = String(s).match(/(\d{1,2}):(\d{2})/);
|
||||
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
|
||||
return null;
|
||||
};
|
||||
const base = tryParse(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
|
||||
if (typeof base === 'number') time = formatTimeFromHM(base + status.delayMinutes);
|
||||
}
|
||||
}
|
||||
const statusClass = status.label === 'supprimé' ? 'aff-classiques-departs__train-meta--suppressed' : (status.label && String(status.label).startsWith('retardé') ? 'aff-classiques-departs__train-meta--delayed' : '');
|
||||
|
||||
// Ne pas afficher le quai si le train est supprimé ou si aucun quai n'est défini
|
||||
let showPlatform = !(status && status.label === 'supprimé') && Boolean(platform);
|
||||
if (showPlatform) {
|
||||
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes(); // Use currentTime
|
||||
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
if (typeof sched === 'number' && typeof threshold === 'number' && threshold > 0) {
|
||||
// n'afficher le quai que si l'heure courante est au moins (scheduled - threshold)
|
||||
showPlatform = (nowHM >= (sched - threshold));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="aff-classiques-departs__row" key={idx}>
|
||||
<div className="aff-classiques-departs__col-logo">
|
||||
<Logo op={op} />
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-train-type">
|
||||
{showStatus ? (
|
||||
<div className={`aff-classiques-departs__train-meta aff-classiques-departs__train-status ${statusClass}`} aria-live="polite">
|
||||
<div className="aff-classiques-departs__op-name">{status.label}</div>
|
||||
<div className="aff-classiques-departs__op-number">{status.detail}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__train-meta">
|
||||
<div className="aff-classiques-departs__op-name">{op}</div>
|
||||
<div className="aff-classiques-departs__op-number">{number}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
</div>
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<div className="aff-classiques-departs__destination">{destination}</div>
|
||||
</div>
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
<div className="aff-classiques-departs__platform-text">{platform}</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre style={{maxHeight:160, overflow:'auto', fontSize:'0.75rem', marginTop:8, background:'#111', color:'#fff', padding:8}}>
|
||||
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).slice(0,1000)}
|
||||
{JSON.stringify(rawDebug.sample.slice(0,2), null, 2).length > 1000 ? '…(truncated)' : ''}
|
||||
</pre>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* Footer visuel de l'afficheur — barre foncée en bas */}
|
||||
<div className="aff-classiques-departs__footer" aria-hidden>
|
||||
<div className="aff-classiques-departs__footer-inner">
|
||||
<Clock />
|
||||
) : horaires.map((h, idx) => renderRow(h, idx))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/* Horloge — superposée en bas à droite */}
|
||||
<div className="aff-classiques-departs__footer" aria-hidden>
|
||||
<Clock />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,475 +1,337 @@
|
||||
|
||||
|
||||
.achemine-font-face {
|
||||
/* helper placeholder (no output) */
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
.aff-quais__left--strict {
|
||||
width: 100%;
|
||||
max-width: 0;
|
||||
min-width: 700px;
|
||||
background: var(--white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
/* Remonter l'ensemble du contenu vers le bas pour laisser la place au logo en haut */
|
||||
padding: 140px 32px 24px 32px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* leave space at bottom for the footer (height 86px) */
|
||||
height: calc(100vh - 86px);
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.aff-quais__left--strict,
|
||||
.aff-quais__left--strict * {
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
/* Ensure the left strict zone text uses the intended color (fallback to green-dark) */
|
||||
color: #2d522c;
|
||||
}
|
||||
.aff-quais__type-logo--strict{
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
/* positionné à gauche dans la zone blanche (à droite du logo opérateur) */
|
||||
/* tout à gauche de la zone blanche */
|
||||
left: 32px;
|
||||
width: 120px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.aff-quais__sncf-logo--strict {
|
||||
width: 86px;
|
||||
height: auto;
|
||||
margin-bottom: 18px;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
/* placé à droite du logo du type */
|
||||
left: 140px;
|
||||
z-index: 4;
|
||||
}
|
||||
.aff-quais__watermark {
|
||||
position: absolute;
|
||||
/* move watermark further to the right */
|
||||
right: -180px;
|
||||
top: 70%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
/* police agrandie de 200% */
|
||||
font-size: 200px;
|
||||
color: var(--watermark-green);
|
||||
opacity: 0.07;
|
||||
font-weight: 800;
|
||||
letter-spacing: 6px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.aff-quais__row--time-status {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__time--strict {
|
||||
color: var(--green-dark);
|
||||
font-weight: 800;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 112px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.5px;
|
||||
margin-right: 12px;
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Wrapper that contains both the big time and the smaller status when alternating.
|
||||
Keeps the layout width/height stable so alternating does not reflow the surrounding text. */
|
||||
.aff-quais__time-status-wrapper {
|
||||
position: relative;
|
||||
display: inline-block; /* width is determined by the time element in normal flow */
|
||||
height: 112px; /* match .aff-quais__time font-size so status can be aligned at bottom */
|
||||
vertical-align: bottom;
|
||||
overflow: visible;
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #006330; /* Green theme */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .aff-quais__time--strict {
|
||||
position: relative; /* keep in flow to define wrapper width */
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: opacity 180ms linear;
|
||||
.aff-quais-arrivees-redesign__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #006330;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .aff-quais__time-status--strict {
|
||||
position: absolute; /* overlay on the same horizontal position */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0; /* align to bottom similar to previous align-self:flex-end */
|
||||
transition: opacity 180ms linear;
|
||||
.aff-quais-arrivees-redesign__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .hidden { opacity: 0; pointer-events: none; }
|
||||
.aff-quais__time-status-wrapper .visible { opacity: 1; }
|
||||
.aff-quais__time-status--strict {
|
||||
color: var(--accent-yellow);
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
font-weight: 400;
|
||||
opacity: 1;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.aff-quais__destination--strict {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
color: var(--green-dark);
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__destination-line1--strict {
|
||||
/* police agrandie de 200% */
|
||||
font-size: 76px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.aff-quais__destination-line2--strict {
|
||||
/* police agrandie de 200% */
|
||||
font-size: 76px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.aff-quais__train-id--strict {
|
||||
margin-top: 12px;
|
||||
color: var(--green-dark);
|
||||
font-weight: 700;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__train-number--strict {
|
||||
color: var(--green-dark);
|
||||
font-weight: 400;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
}
|
||||
.aff-quais__quai{
|
||||
margin-top: 8px;
|
||||
color: var(--green-dark);
|
||||
font-weight:800;
|
||||
background: rgba(255,255,255,0.08);
|
||||
padding:6px 10px;
|
||||
border-radius:6px;
|
||||
display:inline-block;
|
||||
.aff-quais-arrivees-redesign__empty-text {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* central divider */
|
||||
.aff-quais__divider{
|
||||
width:18px;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.18));
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:flex-start;
|
||||
padding-top:36px;
|
||||
}
|
||||
.aff-quais__divider:before{
|
||||
content:'';
|
||||
width:6px;
|
||||
background: transparent;
|
||||
box-shadow: 0 18px 0 0 #f0e34f, 0 54px 0 0 #f0e34f, 0 90px 0 0 #f0e34f, 0 126px 0 0 #f0e34f, 0 162px 0 0 #f0e34f, 0 198px 0 0 #f0e34f;
|
||||
display:block;
|
||||
border-radius:4px;
|
||||
/* =========================================================
|
||||
LEFT PANEL
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35%;
|
||||
bottom: 80px;
|
||||
background-color: #f3eef4; /* Lilac/Light Grey background */
|
||||
box-sizing: border-box;
|
||||
padding: 30px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Right panel with stops */
|
||||
.aff-quais__right{
|
||||
flex:1;
|
||||
background: linear-gradient(180deg,var(--green) 0%, var(--green-dark) 100%);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
box-sizing:border-box;
|
||||
padding: 24px 36px;
|
||||
}
|
||||
.aff-quais__stops-box{
|
||||
width:100%;
|
||||
max-width:820px;
|
||||
margin-left:24px;
|
||||
}
|
||||
.aff-quais__stops-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
width:100%;
|
||||
}
|
||||
.aff-quais__stop{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:18px;
|
||||
padding: 10px 16px;
|
||||
color:#fff;
|
||||
font-size:30px;
|
||||
line-height:1.1;
|
||||
}
|
||||
.aff-quais__bullet{
|
||||
width:14px;
|
||||
height:14px;
|
||||
background: #bbd258; /* yellow bullet */
|
||||
border-radius:50%;
|
||||
display:inline-block;
|
||||
box-shadow: 0 0 0 4px rgba(240,227,79,0.06) inset;
|
||||
}
|
||||
.aff-quais__stop--active{
|
||||
background: rgba(155,123,224,0.15);
|
||||
border-radius:6px;
|
||||
}
|
||||
.aff-quais__stop--active .aff-quais__bullet{
|
||||
background:#9b7be0; /* purple for final */
|
||||
}
|
||||
.aff-quais__stop-text{
|
||||
flex:1;
|
||||
padding-left:6px;
|
||||
.aff-quais-arrivees-redesign__sncf-logo {
|
||||
width: 90px;
|
||||
height: auto;
|
||||
margin-bottom: 25px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* --- STRICT STYLES FOR RIGHT PANEL --- */
|
||||
|
||||
.aff-quais__right--strict {
|
||||
flex: 1;
|
||||
/* use arrivals green gradient */
|
||||
background: linear-gradient(180deg, var(--green) -100%, var(--green-dark) 100%);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 48px 0 48px 0;
|
||||
min-width: 0;
|
||||
/* leave space for footer */
|
||||
height: calc(100vh - 86px);
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
box-shadow: none;
|
||||
position: relative; /* anchor for the stops rail */
|
||||
.aff-quais-arrivees-redesign__time-status {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais__stops-list--strict {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
/* variables pour contrôler la colonne de pastilles */
|
||||
--rail-gap: 72px; /* espacement vertical entre pastilles (ajuster si nécessaire) */
|
||||
--rail-offset: 24px; /* décalage vertical initial pour aligner la première pastille */
|
||||
--rail-left: 28px; /* position X du rail inside right panel */
|
||||
--rail-width: 12px; /* largeur du rail/pastille */
|
||||
--ul-padding: 56px; /* padding-left actuel de la liste */
|
||||
/* laisser un espace modéré à gauche pour la colonne de points */
|
||||
padding: 0 0 0 var(--ul-padding);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
.aff-quais-arrivees-redesign__time {
|
||||
color: #006330; /* Green theme text */
|
||||
font-weight: 700;
|
||||
font-size: 7.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* élément rail explicite (utilisé pour afficher la colonne de pastilles) */
|
||||
.aff-quais__stops-rail{
|
||||
position: absolute;
|
||||
/* position relative to the right panel container */
|
||||
left: calc(var(--rail-left) - 8px);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/* make rail clearly visible and wide enough */
|
||||
width: 30px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
/* base color: same as destination rectangle */
|
||||
background-color: rgba(255,255,255,0.06);
|
||||
overflow: visible;
|
||||
.aff-quais-arrivees-redesign__status {
|
||||
color: #006330; /* Green theme status */
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-quais__rail-dot{
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* size is set dynamically from JS to match rail width */
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-yellow);
|
||||
box-shadow: 0 0 0 4px rgba(255,230,102,0.06) inset;
|
||||
.aff-quais-arrivees-redesign__destination {
|
||||
color: #006330; /* Green theme destination (origin) */
|
||||
font-weight: 700;
|
||||
font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
|
||||
line-height: 1.1;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
white-space: normal; /* Allow wrapping */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 13rem; /* Prevent overflowing panel */
|
||||
}
|
||||
|
||||
.aff-quais__stop--strict {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* agrandi de 150% */
|
||||
font-size: calc(2.1rem * 1.5);
|
||||
color: #fff;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
/* espacement augmenté entre les noms des gares */
|
||||
margin-bottom: 18px;
|
||||
line-height: 1.18;
|
||||
padding: 0 0 0 0;
|
||||
position: relative; /* each li is positioned so its ::before aligns with the rail */
|
||||
.aff-quais-arrivees-redesign__train-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais__stop-bullet--strict {
|
||||
/* bullets individuels masqués : on utilise une barre verticale */
|
||||
display: none;
|
||||
.aff-quais-arrivees-redesign__train-type {
|
||||
color: #006330; /* Green theme train type */
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* pastille positionnée pour chaque item, alignée sur la colonne (rail) */
|
||||
.aff-quais__stop--strict::before{
|
||||
/* disable per-item pseudo pastille — rail handles the dots */
|
||||
display: none;
|
||||
.aff-quais-arrivees-redesign__train-number {
|
||||
color: #7e8a80; /* Grey/Green train number */
|
||||
font-weight: 400;
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* barre verticale de points jaunes pour la liste des gares */
|
||||
/* removed ::before — using explicit .aff-quais__stops-rail element instead */
|
||||
|
||||
.aff-quais__stop-text--strict{ /* ensure text doesn't overlap the rail */
|
||||
padding-left: 6px;
|
||||
.aff-quais-arrivees-redesign__icons {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais__status--alert{
|
||||
color: #ffe600;
|
||||
font-weight: 800;
|
||||
.aff-quais-arrivees-redesign__watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: right center;
|
||||
font-size: 17rem; /* Massive size */
|
||||
color: #dbebdf; /* Light green/grey watermark */
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
letter-spacing: 1.5rem;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-quais__stop-text--strict {
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
/* =========================================================
|
||||
RIGHT PANEL
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 65%;
|
||||
bottom: 80px;
|
||||
background-color: #006330; /* Green theme */
|
||||
box-sizing: border-box;
|
||||
padding: 30px 0 30px 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais__stop--strict-dest {
|
||||
font-weight: 800;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: var(--green-dark);
|
||||
border-radius: 6px;
|
||||
padding: 2px 32px 2px 0;
|
||||
margin-left: -45px;
|
||||
width: auto;
|
||||
|
||||
padding-left: 45px;
|
||||
.aff-quais-arrivees-redesign__provenance-title {
|
||||
font-size: 3.4rem;
|
||||
color: #dbeb1a; /* Yellow provenance header */
|
||||
margin-left: 60px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Ensure the highlighted destination stop text is white for readability */
|
||||
.aff-quais__stop--strict-dest .aff-quais__stop-text--strict {
|
||||
color: #ffffff !important;
|
||||
.aff-quais-arrivees-redesign__stops-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 60px; /* Space from left edge */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* Closer spacing between stops */
|
||||
}
|
||||
|
||||
.aff-quais__container--strict.aff-quais__container--arrivees {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
background: var(--green-dark);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* make container positioned so the absolute footer can be placed relative to it */
|
||||
position: relative;
|
||||
.aff-quais-arrivees-redesign__stop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
padding-left: 50px; /* Space for rail and bullet */
|
||||
}
|
||||
|
||||
.aff-quais__separator--strict {
|
||||
width: 14px;
|
||||
min-width: 14px;
|
||||
max-width: 14px;
|
||||
background: var(--green-dark);
|
||||
/* align separator with the panels which leave room for footer */
|
||||
height: calc(100vh - 86px);
|
||||
display: block;
|
||||
z-index: 2;
|
||||
/* Bullet styling */
|
||||
.aff-quais-arrivees-redesign__stop-bullet {
|
||||
position: absolute;
|
||||
left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
|
||||
margin-left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px; /* Slightly larger bullet */
|
||||
height: 16px;
|
||||
background-color: #fbec22;
|
||||
border-radius: 50%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.aff-quais__right--strict {
|
||||
padding: 18px 0 18px 0;
|
||||
}
|
||||
/* moins d'espace en haut sur petits écrans */
|
||||
.aff-quais__left--strict { padding-top: 80px; }
|
||||
.aff-quais__stops-list--strict {
|
||||
padding-left: 12px;
|
||||
}
|
||||
.aff-quais__stop--strict {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.aff-quais__stop-bullet--strict {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.aff-quais__stop--strict-dest {
|
||||
padding: 1px 6px 1px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.aff-quais-arrivees-redesign__stop-text {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.5px;
|
||||
padding-left: 10px; /* Space from bullet */
|
||||
}
|
||||
|
||||
/* --- END STRICT STYLES FOR RIGHT PANEL --- */
|
||||
|
||||
.aff-quais__footer{
|
||||
position:absolute;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
height:86px;
|
||||
pointer-events:none;
|
||||
}
|
||||
.aff-quais__footer-inner{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
height:100%;
|
||||
padding: 0 28px;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.15), rgba(0,0,0,0.25));
|
||||
}
|
||||
.aff-quais__footer-left{
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:20px;
|
||||
}
|
||||
/* CLOCK BOX - unified */
|
||||
.aff-quais__clock-box{
|
||||
background: var(--green-dark);
|
||||
color:#fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 4px solid #ffffff;
|
||||
}
|
||||
.aff-quais__clock-main{
|
||||
font-size:18px;
|
||||
font-weight:800;
|
||||
/* The vertical rail segments drawn behind each bullet */
|
||||
.aff-quais-arrivees-redesign__stop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px; /* Much wider rail */
|
||||
background-color: #8fa993; /* Light green-grey rail color */
|
||||
top: -10px;
|
||||
bottom: -10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* position the clock bottom-right inside a small white rounded box like on the image */
|
||||
.aff-quais__footer-right{
|
||||
position:relative;
|
||||
}
|
||||
.aff-quais__clock{
|
||||
position:relative;
|
||||
}
|
||||
@media (max-width: 900px){
|
||||
.aff-quais__left{width:42%;}
|
||||
.aff-quais__destination-line1{font-size:36px}
|
||||
.aff-quais__destination-line2{font-size:28px}
|
||||
.aff-quais__time{font-size:48px}
|
||||
/* Rail starts at the first bullet's center */
|
||||
.aff-quais-arrivees-redesign__stop:first-child::before {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
/* Last stop has no rail continuing below */
|
||||
.aff-quais-arrivees-redesign__stop:last-child::before {
|
||||
bottom: 50%;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: #006330; /* Green theme */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__footer-left {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-style: italic;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__clock {
|
||||
border: 2px solid #ffffff;
|
||||
padding: 4px 14px;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__clock-s {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--long-text {
|
||||
font-size: 3.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--suppressed,
|
||||
.aff-quais-arrivees-redesign__status--suppressed {
|
||||
color: #ff4c5b !important;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--delayed,
|
||||
.aff-quais-arrivees-redesign__status--delayed {
|
||||
color: #ff9e43 !important;
|
||||
}
|
||||
|
||||
/* Disrupted Stop styling */
|
||||
.aff-quais-arrivees-redesign__stop-text--suppressed {
|
||||
text-decoration: line-through;
|
||||
color: #ff4c5b;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-text--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge {
|
||||
font-size: 2.2rem;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge--suppressed {
|
||||
color: #ff4c5b;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,3 @@
|
||||
/* Partie gauche strictement comme l'image */
|
||||
.achemine-font-face {
|
||||
/* helper placeholder (no output) */
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
@@ -16,473 +12,333 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
.aff-quais__left--strict {
|
||||
width: 100%;
|
||||
max-width: 0;
|
||||
min-width: 700px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
/* Remonter l'ensemble du contenu vers le bas pour laisser la place au logo en haut */
|
||||
padding: 140px 32px 24px 32px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
/* leave space at bottom for the footer (height 86px) */
|
||||
height: calc(100vh - 86px);
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.aff-quais__left--strict,
|
||||
.aff-quais__left--strict * {
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Apply blue color specifically for departures container only (avoid being overridden by arrivées stylesheet)
|
||||
Use :not(.aff-quais__container--arrivees) to scope to departures */
|
||||
.aff-quais__container--strict:not(.aff-quais__container--arrivees) .aff-quais__left--strict,
|
||||
.aff-quais__container--strict:not(.aff-quais__container--arrivees) .aff-quais__left--strict * {
|
||||
color: #0d4a87;
|
||||
}
|
||||
|
||||
/* Strong override for departures container: ensure destination title isn't black/green due to other styles */
|
||||
.aff-quais__container--depart .aff-quais__left--strict,
|
||||
.aff-quais__container--depart .aff-quais__left--strict * {
|
||||
color: #0d4a87 !important;
|
||||
}
|
||||
.aff-quais__type-logo--strict{
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
/* positionné à gauche dans la zone blanche (à droite du logo opérateur) */
|
||||
/* tout à gauche de la zone blanche */
|
||||
left: 32px;
|
||||
width: 120px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.aff-quais__sncf-logo--strict {
|
||||
width: 86px;
|
||||
height: auto;
|
||||
margin-bottom: 18px;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
/* placé à droite du logo du type */
|
||||
left: 140px;
|
||||
z-index: 4;
|
||||
}
|
||||
.aff-quais__watermark {
|
||||
position: absolute;
|
||||
/* move watermark further to the right */
|
||||
right: -180px;
|
||||
top: 70%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
/* police agrandie de 200% */
|
||||
font-size: 200px;
|
||||
color: rgba(23,55,113,0.07);
|
||||
font-weight: 800;
|
||||
letter-spacing: 6px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.aff-quais__row--time-status {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__time--strict {
|
||||
color: #0d4a87;
|
||||
font-weight: 800;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 112px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.5px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* Wrapper that contains both the big time and the smaller status when alternating.
|
||||
Keeps the layout width/height stable so alternating does not reflow the surrounding text. */
|
||||
.aff-quais__time-status-wrapper {
|
||||
position: relative;
|
||||
display: inline-block; /* width is determined by the time element in normal flow */
|
||||
height: 112px; /* match .aff-quais__time font-size so status can be aligned at bottom */
|
||||
vertical-align: bottom;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .aff-quais__time--strict {
|
||||
position: relative; /* keep in flow to define wrapper width */
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: opacity 180ms linear;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .aff-quais__time-status--strict {
|
||||
position: absolute; /* overlay on the same horizontal position */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0; /* align to bottom similar to previous align-self:flex-end */
|
||||
transition: opacity 180ms linear;
|
||||
}
|
||||
|
||||
.aff-quais__time-status-wrapper .hidden { opacity: 0; pointer-events: none; }
|
||||
.aff-quais__time-status-wrapper .visible { opacity: 1; }
|
||||
.aff-quais__time-status--strict {
|
||||
color: #0d4a87;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
font-weight: 400;
|
||||
opacity: 1;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.aff-quais__destination--strict {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
color: #0d4a87;
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__destination-line1--strict {
|
||||
/* police agrandie de 200% */
|
||||
font-size: 76px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.aff-quais__destination-line2--strict {
|
||||
/* police agrandie de 200% */
|
||||
font-size: 76px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.aff-quais__train-id--strict {
|
||||
margin-top: 12px;
|
||||
color: #0d4a87;
|
||||
font-weight: 700;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 1;
|
||||
}
|
||||
.aff-quais__train-number--strict {
|
||||
color: #0d4a87;
|
||||
font-weight: 400;
|
||||
/* police agrandie de 200% */
|
||||
font-size: 44px;
|
||||
}
|
||||
.aff-quais__quai{
|
||||
margin-top: 8px;
|
||||
color:#0d4a87;
|
||||
font-weight:800;
|
||||
background: rgba(23,55,113,0.08);
|
||||
padding:6px 10px;
|
||||
border-radius:6px;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
/* central divider */
|
||||
.aff-quais__divider{
|
||||
width:18px;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.12), rgba(255, 255, 255, 0.18));
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:flex-start;
|
||||
padding-top:36px;
|
||||
}
|
||||
.aff-quais__divider:before{
|
||||
content:'';
|
||||
width:6px;
|
||||
background: transparent;
|
||||
box-shadow: 0 18px 0 0 #f0e34f, 0 54px 0 0 #f0e34f, 0 90px 0 0 #f0e34f, 0 126px 0 0 #f0e34f, 0 162px 0 0 #f0e34f, 0 198px 0 0 #f0e34f;
|
||||
display:block;
|
||||
border-radius:4px;
|
||||
}
|
||||
|
||||
/* Right panel with stops */
|
||||
.aff-quais__right{
|
||||
flex:1;
|
||||
background: linear-gradient(180deg,#205aa1 0%, #144b85 100%);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
box-sizing:border-box;
|
||||
padding: 24px 36px;
|
||||
}
|
||||
.aff-quais__stops-box{
|
||||
width:100%;
|
||||
max-width:820px;
|
||||
margin-left:24px;
|
||||
}
|
||||
.aff-quais__stops-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
width:100%;
|
||||
}
|
||||
.aff-quais__stop{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:18px;
|
||||
padding: 10px 16px;
|
||||
color:#fff;
|
||||
font-size:30px;
|
||||
line-height:1.1;
|
||||
}
|
||||
.aff-quais__bullet{
|
||||
width:14px;
|
||||
height:14px;
|
||||
background: #bbd258; /* yellow bullet */
|
||||
border-radius:50%;
|
||||
display:inline-block;
|
||||
box-shadow: 0 0 0 4px rgba(240,227,79,0.06) inset;
|
||||
}
|
||||
.aff-quais__stop--active{
|
||||
background: rgba(155,123,224,0.15);
|
||||
border-radius:6px;
|
||||
}
|
||||
.aff-quais__stop--active .aff-quais__bullet{
|
||||
background:#9b7be0; /* purple for final */
|
||||
}
|
||||
.aff-quais__stop-text{
|
||||
flex:1;
|
||||
padding-left:6px;
|
||||
}
|
||||
|
||||
/* --- STRICT STYLES FOR RIGHT PANEL --- */
|
||||
|
||||
.aff-quais__right--strict {
|
||||
flex: 1;
|
||||
background: #205aa1; /* fond bleu uniforme */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 48px 0 48px 0;
|
||||
min-width: 0;
|
||||
/* leave space for footer */
|
||||
height: calc(100vh - 86px);
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
box-shadow: none;
|
||||
position: relative; /* anchor for the stops rail */
|
||||
}
|
||||
|
||||
.aff-quais__stops-list--strict {
|
||||
list-style: none;
|
||||
body, html {
|
||||
margin: 0;
|
||||
/* variables pour contrôler la colonne de pastilles */
|
||||
--rail-gap: 72px; /* espacement vertical entre pastilles (ajuster si nécessaire) */
|
||||
--rail-offset: 24px; /* décalage vertical initial pour aligner la première pastille */
|
||||
--rail-left: 28px; /* position X du rail inside right panel */
|
||||
--rail-width: 12px; /* largeur du rail/pastille */
|
||||
--ul-padding: 56px; /* padding-left actuel de la liste */
|
||||
/* laisser un espace modéré à gauche pour la colonne de points */
|
||||
padding: 0 0 0 var(--ul-padding);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
background-color: #0b376d;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* élément rail explicite (utilisé pour afficher la colonne de pastilles) */
|
||||
.aff-quais__stops-rail{
|
||||
position: absolute;
|
||||
/* position relative to the right panel container */
|
||||
left: calc(var(--rail-left) - 8px);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/* make rail clearly visible and wide enough */
|
||||
width: 30px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
/* base color: same as destination rectangle */
|
||||
background-color: #b6d2f7;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aff-quais__rail-dot{
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* size is set dynamically from JS to match rail width */
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
background: #b9d25a;
|
||||
box-shadow: 0 0 0 4px rgba(255,230,102,0.06) inset;
|
||||
}
|
||||
|
||||
.aff-quais__stop--strict {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* agrandi de 150% */
|
||||
font-size: calc(2.1rem * 1.5);
|
||||
color: #fff;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
/* espacement augmenté entre les noms des gares */
|
||||
margin-bottom: 18px;
|
||||
line-height: 1.18;
|
||||
padding: 0 0 0 0;
|
||||
position: relative; /* each li is positioned so its ::before aligns with the rail */
|
||||
}
|
||||
|
||||
.aff-quais__stop-bullet--strict {
|
||||
/* bullets individuels masqués : on utilise une barre verticale */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* pastille positionnée pour chaque item, alignée sur la colonne (rail) */
|
||||
.aff-quais__stop--strict::before{
|
||||
/* disable per-item pseudo pastille — rail handles the dots */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* barre verticale de points jaunes pour la liste des gares */
|
||||
/* removed ::before — using explicit .aff-quais__stops-rail element instead */
|
||||
|
||||
.aff-quais__stop-text--strict{ /* ensure text doesn't overlap the rail */
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.aff-quais__status--alert{
|
||||
color: #ffe600;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.aff-quais__stop-text--strict {
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.aff-quais__stop--strict-dest {
|
||||
font-weight: 800;
|
||||
background: #b6d2f7;
|
||||
color: #205aa1;
|
||||
border-radius: 6px;
|
||||
padding: 2px 32px 2px 0;
|
||||
margin-left: -45px;
|
||||
width: auto;
|
||||
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
/* Ensure the highlighted destination stop text is always white for readability */
|
||||
.aff-quais__stop--strict-dest .aff-quais__stop-text--strict {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.aff-quais__container--strict {
|
||||
.aff-quais-departs-redesign__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
/* Palette locale pour l'afficheur départs : évite la palette arrivées importée ailleurs */
|
||||
--blue: #205aa1;
|
||||
--blue-dark: #144b85;
|
||||
--accent-yellow: #e1c14c;
|
||||
--white: #ffffff;
|
||||
--watermark-blue: rgba(23,55,113,0.07);
|
||||
background: var(--blue);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* make container positioned so the absolute footer can be placed relative to it */
|
||||
position: relative;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #0b376d;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais__separator--strict {
|
||||
width: 14px;
|
||||
min-width: 14px;
|
||||
max-width: 14px;
|
||||
background: #205aa1;
|
||||
/* align separator with the panels which leave room for footer */
|
||||
height: calc(100vh - 86px);
|
||||
display: block;
|
||||
.aff-quais-departs-redesign__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__empty-text {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LEFT PANEL
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35%;
|
||||
bottom: 80px;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__sncf-logo {
|
||||
width: 90px;
|
||||
height: auto;
|
||||
margin-bottom: 25px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.aff-quais__right--strict {
|
||||
padding: 18px 0 18px 0;
|
||||
}
|
||||
/* moins d'espace en haut sur petits écrans */
|
||||
.aff-quais__left--strict { padding-top: 80px; }
|
||||
.aff-quais__stops-list--strict {
|
||||
padding-left: 12px;
|
||||
}
|
||||
.aff-quais__stop--strict {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.aff-quais__stop-bullet--strict {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.aff-quais__stop--strict-dest {
|
||||
padding: 1px 6px 1px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.aff-quais-departs-redesign__time-status {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* --- END STRICT STYLES FOR RIGHT PANEL --- */
|
||||
|
||||
.aff-quais__footer{
|
||||
position:absolute;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
height:86px;
|
||||
pointer-events:none;
|
||||
}
|
||||
.aff-quais__footer-inner{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
height:100%;
|
||||
padding: 0 28px;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.15), rgba(0,0,0,0.25));
|
||||
}
|
||||
.aff-quais__footer-left{
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:20px;
|
||||
}
|
||||
/* CLOCK BOX - unified */
|
||||
.aff-quais__clock-box{
|
||||
background: rgba(23,55,113,0.9);
|
||||
color:#fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 4px solid #ffffff;
|
||||
}
|
||||
.aff-quais__clock-main{
|
||||
font-size:18px;
|
||||
font-weight:800;
|
||||
.aff-quais-departs-redesign__time {
|
||||
color: #0b376d;
|
||||
font-weight: 700;
|
||||
font-size: 7.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* position the clock bottom-right inside a small white rounded box like on the image */
|
||||
.aff-quais__footer-right{
|
||||
position:relative;
|
||||
}
|
||||
.aff-quais__clock{
|
||||
position:relative;
|
||||
}
|
||||
@media (max-width: 900px){
|
||||
.aff-quais__left{width:42%;}
|
||||
.aff-quais__destination-line1{font-size:36px}
|
||||
.aff-quais__destination-line2{font-size:28px}
|
||||
.aff-quais__time{font-size:48px}
|
||||
.aff-quais-departs-redesign__status {
|
||||
color: #0b376d;
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__destination {
|
||||
color: #0b376d;
|
||||
font-weight: 700;
|
||||
font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
|
||||
line-height: 1.1;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
white-space: normal; /* Allow wrapping */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 13rem; /* Prevent overflowing panel */
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-type {
|
||||
color: #0b376d;
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-number {
|
||||
color: #929bb0;
|
||||
font-weight: 400;
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__icons {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: right center;
|
||||
font-size: 17rem; /* Massive size */
|
||||
color: #e5e8ed;
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
letter-spacing: 1.5rem;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
RIGHT PANEL
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 65%;
|
||||
bottom: 80px;
|
||||
background-color: #0b376d;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 0 30px 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stops-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 60px; /* Space from left edge */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* Closer spacing between stops */
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
padding-left: 50px; /* Space for rail and bullet */
|
||||
}
|
||||
|
||||
/* Bullet styling */
|
||||
.aff-quais-departs-redesign__stop-bullet {
|
||||
position: absolute;
|
||||
left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
|
||||
margin-left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px; /* Slightly larger bullet */
|
||||
height: 16px;
|
||||
background-color: #fbec22;
|
||||
border-radius: 50%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-text {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.5px;
|
||||
padding-left: 10px; /* Space from bullet */
|
||||
}
|
||||
|
||||
/* The vertical rail segments drawn behind each bullet */
|
||||
.aff-quais-departs-redesign__stop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px; /* Much wider rail */
|
||||
background-color: #5c538f;
|
||||
top: -10px;
|
||||
bottom: -10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Rail extends all the way to the top */
|
||||
.aff-quais-departs-redesign__stop:first-child::before {
|
||||
top: -1000px;
|
||||
}
|
||||
|
||||
/* Last stop has no rail continuing below */
|
||||
.aff-quais-departs-redesign__stop:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Highlighted Destination Stop */
|
||||
.aff-quais-departs-redesign__stop--dest {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop--dest .aff-quais-departs-redesign__stop-text {
|
||||
background-color: #5c538f;
|
||||
padding: 4px 20px 4px 60px; /* Increased left padding to cover bullet */
|
||||
margin-left: -60px; /* Pull back to exactly align with left:0 */
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 3; /* Behind the bullet (z-index 4) */
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: #0b376d;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__footer-left {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-style: italic;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__clock {
|
||||
border: 2px solid #ffffff;
|
||||
padding: 4px 14px;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__clock-s {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--long-text {
|
||||
font-size: 3.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--suppressed,
|
||||
.aff-quais-departs-redesign__status--suppressed {
|
||||
color: #ff4c5b !important;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--delayed,
|
||||
.aff-quais-departs-redesign__status--delayed {
|
||||
color: #ff9e43 !important;
|
||||
}
|
||||
|
||||
/* Disrupted Stop styling */
|
||||
.aff-quais-departs-redesign__stop-text--suppressed {
|
||||
text-decoration: line-through;
|
||||
color: #ff4c5b;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-text--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge {
|
||||
font-size: 2.2rem;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge--suppressed {
|
||||
color: #ff4c5b;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
@@ -1,70 +1,6 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import './ClassiquesDepartsQuais.css';
|
||||
import supabase from '../../../../lib/supabase';
|
||||
|
||||
// Hook pour récupérer la map des logos depuis /type/data.json
|
||||
function useTypeLogos(){
|
||||
const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const load = async () => {
|
||||
const candidates = ['/type/quais/data-color.json', '/type/data.json'];
|
||||
for(const url of candidates){
|
||||
try{
|
||||
const res = await fetch(url);
|
||||
if(!res.ok) continue;
|
||||
const data = await res.json();
|
||||
if(!mounted) return;
|
||||
const logos = {};
|
||||
if(Array.isArray(data.logos)){
|
||||
data.logos.forEach(item => {
|
||||
// item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path
|
||||
let file = item.file;
|
||||
if(!file && item.path){
|
||||
const parts = String(item.path).split('/');
|
||||
file = parts[parts.length-1];
|
||||
}
|
||||
if(!file) return;
|
||||
const basename = file.replace(/\.[^/.]+$/, '');
|
||||
const semantic = basename.replace(/^logo-/, '');
|
||||
const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');
|
||||
// Prefer color logos in the 'quais' subfolder if data came from the quais file
|
||||
if(url.includes('/quais/')){
|
||||
logos[key] = `/type/quais/${file}`;
|
||||
} else {
|
||||
logos[key] = `/type/${file}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';
|
||||
setMap(logos);
|
||||
return;
|
||||
}catch(e){
|
||||
// try next candidate (ignore error and continue loop)
|
||||
}
|
||||
}
|
||||
// no candidate succeeded — keep fallback
|
||||
};
|
||||
|
||||
load();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
return map;
|
||||
}
|
||||
|
||||
function getTypeLogo(type, TYPE_LOGOS_MAP){
|
||||
const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };
|
||||
if(!type) return map.default;
|
||||
const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');
|
||||
if(map[norm]) return map[norm];
|
||||
const keys = Object.keys(map).filter(k => k !== 'default');
|
||||
for(const k of keys){
|
||||
if(k.includes(norm) || norm.includes(k)) return map[k];
|
||||
}
|
||||
return map.default;
|
||||
}
|
||||
|
||||
function Clock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
@@ -75,20 +11,15 @@ function Clock() {
|
||||
const mm = String(now.getMinutes()).padStart(2,'0');
|
||||
const ss = String(now.getSeconds()).padStart(2,'0');
|
||||
return (
|
||||
<div className="aff-quais__clock">
|
||||
<div className="aff-quais__clock-box"><span className="aff-quais__clock-main">{hh}:{mm}:{ss}</span></div>
|
||||
<div className="aff-quais-departs-redesign__clock">
|
||||
<span>{hh}</span>
|
||||
<span>{mm}</span>
|
||||
<span className="aff-quais-departs-redesign__clock-s">{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesDepartsQuais(){
|
||||
const typeLogos = useTypeLogos();
|
||||
|
||||
const rightRef = useRef(null);
|
||||
const stopsListRef = useRef(null);
|
||||
const railRef = useRef(null);
|
||||
|
||||
// fallback mock strict (utilisé uniquement si aucune donnée réelle n'est trouvée)
|
||||
export default function ClassiquesDepartsQuais() {
|
||||
const mock = {
|
||||
time: '16h44',
|
||||
status: "à l'heure",
|
||||
@@ -101,26 +32,37 @@ export default function ClassiquesDepartsQuais(){
|
||||
],
|
||||
};
|
||||
|
||||
// état pour l'horaire sélectionné
|
||||
const [selectedHoraire, setSelectedHoraire] = useState(null);
|
||||
const [loadingHoraire, setLoadingHoraire] = useState(false);
|
||||
const [horaireError, setHoraireError] = useState(null);
|
||||
const [candidatesCount, setCandidatesCount] = useState(0);
|
||||
// type de la gare (récupéré depuis la table gares)
|
||||
const [, setHoraireError] = useState(null);
|
||||
const [, setCandidatesCount] = useState(0);
|
||||
const [stationType, setStationType] = useState(null);
|
||||
// Heure actuelle mise à jour chaque seconde pour la logique de seuil
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [alternateDelayPhase, setAlternateDelayPhase] = useState(true);
|
||||
const [alternateCancelPhase, setAlternateCancelPhase] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// perturbations/travaux (récupérées depuis Supabase)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setAlternateDelayPhase(prev => !prev);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setAlternateCancelPhase(prev => !prev);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const [perturbations, setPerturbations] = useState([]);
|
||||
const [, setTravaux] = useState([]);
|
||||
|
||||
// helpers locaux (simples) — similaires à ceux du composant principal
|
||||
const weekdayNameForColumn = useCallback((d) => {
|
||||
const i = d.getDay();
|
||||
switch(i) {
|
||||
@@ -141,20 +83,19 @@ export default function ClassiquesDepartsQuais(){
|
||||
const day = d.getDate();
|
||||
const s = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const holidays = [
|
||||
`${y}-01-01`, // Jour de l'an
|
||||
`${y}-05-01`, // Fête du Travail
|
||||
`${y}-05-08`, // Victoire 1945
|
||||
`${y}-07-14`, // Fête nationale
|
||||
`${y}-08-15`, // Assomption
|
||||
`${y}-11-01`, // Toussaint
|
||||
`${y}-11-11`, // Armistice 1918
|
||||
`${y}-12-25`, // Noël
|
||||
`${y}-01-01`,
|
||||
`${y}-05-01`,
|
||||
`${y}-05-08`,
|
||||
`${y}-07-14`,
|
||||
`${y}-08-15`,
|
||||
`${y}-11-01`,
|
||||
`${y}-11-11`,
|
||||
`${y}-12-25`,
|
||||
];
|
||||
// Dates variables pour 2026
|
||||
if (y === 2026) {
|
||||
holidays.push('2026-04-06'); // Lundi de Pâques
|
||||
holidays.push('2026-05-14'); // Ascension
|
||||
holidays.push('2026-05-25'); // Lundi de Pentecôte
|
||||
holidays.push('2026-04-06');
|
||||
holidays.push('2026-05-14');
|
||||
holidays.push('2026-05-25');
|
||||
}
|
||||
return holidays.includes(s);
|
||||
}, []);
|
||||
@@ -215,8 +156,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
return String(v).slice(0,5).replace(':','h');
|
||||
}, []);
|
||||
|
||||
// ...existing code...
|
||||
|
||||
const formatTimeFromHM = useCallback((totalMinutes) => {
|
||||
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
|
||||
const m = ((totalMinutes % 1440) + 1440) % 1440;
|
||||
@@ -225,24 +164,21 @@ export default function ClassiquesDepartsQuais(){
|
||||
return `${hh}h${mm}`;
|
||||
}, []);
|
||||
|
||||
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
|
||||
const thresholdForStationType = useCallback((type) => {
|
||||
if (!type) return 0;
|
||||
const t = String(type).trim().toLowerCase();
|
||||
if (t === 'interurbaine' || t.includes('interurb')) return 360; // 6 heures
|
||||
if (t === 'ville' || t.includes('ville')) return 30; // 30 minutes
|
||||
if (t === 'interurbaine' || t.includes('interurb')) return 360;
|
||||
if (t === 'ville' || t.includes('ville')) return 30;
|
||||
return 0;
|
||||
}, []);
|
||||
|
||||
// lecture des query params pour gare & quai
|
||||
const paramsForThis = new URLSearchParams(window.location.search || '');
|
||||
const gareParam = paramsForThis.get('gare');
|
||||
const gareIdParam = paramsForThis.get('gare_id') || paramsForThis.get('gareId');
|
||||
const quaiParam = paramsForThis.get('quai');
|
||||
const debugEnabled = (paramsForThis.get('debug') === '1' || String(paramsForThis.get('debug')).toLowerCase() === 'true');
|
||||
|
||||
const gareDisplayParam = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g,' ')).replace(/-/g,' ') : null;
|
||||
|
||||
// helper: obtenir la liste d'arrêts d'un horaire (supporte plusieurs noms de champs)
|
||||
const getStops = (h) => {
|
||||
if (!h) return [];
|
||||
if (Array.isArray(h.stops) && h.stops.length) return h.stops;
|
||||
@@ -255,13 +191,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
if (!raw) continue;
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (typeof raw === 'string') {
|
||||
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) { /* ignore */ }
|
||||
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch (e) {}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// --- Helpers pour perturbations (copiées/adaptées depuis ClassiquesDeparts) ---
|
||||
const safeParseArrets = (p) => {
|
||||
const raw = p && (p.perturbations_arrets || p.arrets || p.arrets_perturbations);
|
||||
if (!raw) return [];
|
||||
@@ -386,7 +321,7 @@ export default function ClassiquesDepartsQuais(){
|
||||
for (const p of found) {
|
||||
try {
|
||||
if (horaireYmd) {
|
||||
const pDate = toYMD(p.date || p.date_service || p.start_date || p.date_debut);
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== horaireYmd) continue;
|
||||
@@ -436,7 +371,15 @@ export default function ClassiquesDepartsQuais(){
|
||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||
}, [findPerturbsForHoraireAndStop, parseDelayMinutesFromArret, getPerturbationJours, normalizeName, parsePerturbArretsObject]);
|
||||
|
||||
// Fetch perturbations & travaux from Supabase periodically
|
||||
const perturbMap = React.useMemo(() => buildPerturbMap(perturbations || []), [perturbations, buildPerturbMap]);
|
||||
const serviceDate = React.useMemo(() => {
|
||||
const todayYmd = new Date().toLocaleDateString('en-CA');
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
|
||||
return selectedHoraire && selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
|
||||
}, [selectedHoraire]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let inFlight = false;
|
||||
@@ -452,19 +395,32 @@ export default function ClassiquesDepartsQuais(){
|
||||
]);
|
||||
if (!mounted) return;
|
||||
if (pError || tError) {
|
||||
// ignore quietly — keep previous data
|
||||
} else {
|
||||
// Filter perturbations to those applicable today
|
||||
const todayYmd = new Date().toLocaleDateString('en-CA');
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
|
||||
|
||||
const filteredPerturb = (perturbData || []).filter(p => {
|
||||
try {
|
||||
const toYMD = (v) => { if (!v) return null; const d = new Date(v); if (isNaN(d.getTime())) return null; return d.toISOString().slice(0,10); };
|
||||
const pDate = toYMD(p.date || p.date_service || p.start_date || p.date_debut);
|
||||
const pDate = toYMD(p.date || p.date_service);
|
||||
const db = toYMD(p.date_debut || p.start_date);
|
||||
const df = toYMD(p.date_fin || p.end_date);
|
||||
if (pDate && pDate !== todayYmd) return false;
|
||||
if (db && df && !(todayYmd >= db && todayYmd <= df)) return false;
|
||||
try { const joursSet = getPerturbationJours(p); if (joursSet && !joursSet.has(todayYmd)) return false; } catch (e) {}
|
||||
|
||||
const appliesToToday = (!pDate || pDate === todayYmd) && (!db || !df || (todayYmd >= db && todayYmd <= df));
|
||||
const appliesToTomorrow = (!pDate || pDate === tomorrowYmd) && (!db || !df || (tomorrowYmd >= db && tomorrowYmd <= df));
|
||||
|
||||
if (!appliesToToday && !appliesToTomorrow) return false;
|
||||
|
||||
try {
|
||||
const joursSet = getPerturbationJours(p);
|
||||
if (joursSet) {
|
||||
const hasToday = joursSet.has(todayYmd);
|
||||
const hasTomorrow = joursSet.has(tomorrowYmd);
|
||||
if (!hasToday && !hasTomorrow) return false;
|
||||
}
|
||||
} catch (e) {}
|
||||
return true;
|
||||
} catch (e) { return true; }
|
||||
});
|
||||
@@ -472,7 +428,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
setTravaux(travauxData || []);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
@@ -482,7 +437,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
return () => { mounted = false; clearInterval(id); };
|
||||
}, [getPerturbationJours]);
|
||||
|
||||
// Annotate selectedHoraire with computed status when perturbations change
|
||||
useEffect(() => {
|
||||
if (!selectedHoraire || !selectedHoraire.horaire) return;
|
||||
const horaire = selectedHoraire.horaire;
|
||||
@@ -491,20 +445,36 @@ export default function ClassiquesDepartsQuais(){
|
||||
const stationName = stop ? (typeof stop === 'string' ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : null;
|
||||
try {
|
||||
const map = buildPerturbMap(perturbations || []);
|
||||
const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, null);
|
||||
// keep original scheduled time for display (_displayTime)
|
||||
|
||||
const todayYmd = new Date().toLocaleDateString('en-CA');
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowYmd = tomorrow.toLocaleDateString('en-CA');
|
||||
const serviceDate = selectedHoraire.isTomorrow ? tomorrowYmd : todayYmd;
|
||||
|
||||
const status = computeStatusForHoraireAtStop(horaire, stationId, stationName, map, serviceDate);
|
||||
const originalBase = parseTimeHM(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure);
|
||||
const displayTimeOriginal = (typeof originalBase === 'number') ? formatTimeFromHM(originalBase) : (selectedHoraire.rawTime ? formatTimeNoSeconds(selectedHoraire.rawTime) : (horaire.heure_depart || horaire.time || horaire.heure ? formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure) : null));
|
||||
// compute updated time (delayed) but store separately — do not override the original displayed time
|
||||
let updatedTime = null;
|
||||
if (status && typeof status.delayMinutes === 'number' && !Number.isNaN(status.delayMinutes) && typeof originalBase === 'number') {
|
||||
updatedTime = formatTimeFromHM(originalBase + status.delayMinutes);
|
||||
}
|
||||
setSelectedHoraire(prev => ({ ...(prev||{}), horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status }, _displayTime: displayTimeOriginal, _updatedTime: updatedTime }));
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// Guard to prevent infinite render loop: only set selectedHoraire if status or time actually changed
|
||||
const statusChanged = !horaire._status || horaire._status.label !== status.label || horaire._status.delayMinutes !== status.delayMinutes;
|
||||
const timeChanged = selectedHoraire._displayTime !== displayTimeOriginal || selectedHoraire._updatedTime !== updatedTime;
|
||||
|
||||
if (statusChanged || timeChanged) {
|
||||
setSelectedHoraire(prev => ({
|
||||
...(prev||{}),
|
||||
horaire: { ...(prev && prev.horaire ? prev.horaire : {}), _status: status },
|
||||
_displayTime: displayTimeOriginal,
|
||||
_updatedTime: updatedTime
|
||||
}));
|
||||
}
|
||||
} catch (e) {}
|
||||
}, [selectedHoraire, perturbations, buildPerturbMap, computeStatusForHoraireAtStop, parseTimeHM, formatTimeFromHM, formatTimeNoSeconds]);
|
||||
|
||||
// récupérer depuis Supabase le prochain départ correspondant au couple (gare, quai)
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
async function loadNextForQuai() {
|
||||
@@ -517,17 +487,23 @@ export default function ClassiquesDepartsQuais(){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Récupérer le type de la gare
|
||||
let fetchedType = null;
|
||||
try {
|
||||
if (gareIdParam) {
|
||||
const { data: g } = await supabase.from('gares').select('type').eq('gare_id', gareIdParam).single();
|
||||
if (g) fetchedType = g.type;
|
||||
} else if (gareDisplayParam) {
|
||||
const { data: g } = await supabase.from('gares').select('type').ilike('nom', `%${gareDisplayParam}%`).limit(1).single();
|
||||
if (g) fetchedType = g.type;
|
||||
const { data: gdata } = await supabase.from('gares').select('type,nom').ilike('nom', `%${gareDisplayParam}%`);
|
||||
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(gareDisplayParam || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
fetchedType = exactMatch ? exactMatch.type : gdata[0].type;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (e) {}
|
||||
if (mounted) setStationType(fetchedType);
|
||||
|
||||
const { data: all, error } = await supabase.from('horaires').select('id, *');
|
||||
@@ -555,15 +531,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
for (const h of all) {
|
||||
if (!h) continue;
|
||||
|
||||
// Filtrage par jour de circulation
|
||||
const circulatesToday = circulatesOn(h, now);
|
||||
const circulatesTomorrow = circulatesOn(h, tomorrow);
|
||||
if (!circulatesToday && !circulatesTomorrow) continue;
|
||||
|
||||
// build stops list (support multiple field names)
|
||||
const stops = getStops(h);
|
||||
|
||||
// compute max index for terminus detection if stops contain index
|
||||
let maxIdx = null;
|
||||
if (Array.isArray(stops) && stops.length) {
|
||||
for (const st of stops) {
|
||||
@@ -574,34 +547,24 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// find matching stop (by name or by gare_id)
|
||||
let stopMatch = null;
|
||||
// check horaire-level depart name first
|
||||
if (normalizedTarget) {
|
||||
// exclude horaires where the target gare is the final destination (terminus)
|
||||
const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
|
||||
if (destNameNorm && (destNameNorm === normalizedTarget || destNameNorm.includes(normalizedTarget) || normalizedTarget.includes(destNameNorm))) {
|
||||
// gare ciblée est la gare d'arrivée finale -> ne pas considérer cet horaire
|
||||
// skip to next horaire
|
||||
} else {
|
||||
const departName = normalizeName(h.gare_depart_nom || h.gare_depart || h.origin_name || '');
|
||||
if (departName && (departName === normalizedTarget || departName.includes(normalizedTarget) || normalizedTarget.includes(departName))) {
|
||||
// créer un stopMatch initial à partir du niveau horaire
|
||||
let initialStop = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart };
|
||||
// si la liste stops contient un objet correspondant à la gare de départ, préférer cet objet
|
||||
if (Array.isArray(stops) && stops.length) {
|
||||
for (const s of stops) {
|
||||
const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.station || '');
|
||||
const nm = normalizeName(rawName);
|
||||
if (!nm) continue;
|
||||
if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
|
||||
// prefer the stops entry (it may contain platform/quai information)
|
||||
// but preserve a horaire-level time if the stop entry lacks heure_depart
|
||||
if (typeof s === 'object') {
|
||||
if (!s.heure_depart && initialStop.heure_depart) s.heure_depart = initialStop.heure_depart;
|
||||
initialStop = s;
|
||||
} else {
|
||||
// s is string name — keep initialStop but set gare_nom
|
||||
initialStop.gare_nom = s;
|
||||
}
|
||||
break;
|
||||
@@ -613,15 +576,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// if gare_id param present, try to match by id as well
|
||||
if (!stopMatch && gareIdParam) {
|
||||
const gid = String(gareIdParam);
|
||||
// check horaire-level id
|
||||
const departId = h.gare_depart_id || h.gare_id || h.origin_id || null;
|
||||
if (departId != null && String(departId) === gid) {
|
||||
stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_id: departId, gare_nom: h.gare_depart_nom || h.gare_depart };
|
||||
}
|
||||
// check stops list ids
|
||||
if (!stopMatch && Array.isArray(stops) && stops.length) {
|
||||
for (const s of stops) {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
@@ -630,35 +590,29 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
}
|
||||
|
||||
// if still no match, try match by normalized name inside stops
|
||||
if (!stopMatch && normalizedTarget && Array.isArray(stops) && stops.length) {
|
||||
for (const s of stops) {
|
||||
// support both string entries and object entries
|
||||
const rawName = (typeof s === 'string') ? s : (s.gare_nom || s.nom || s.name || s.stop_name || s.stopName || s.station || s.station_name || '');
|
||||
const nm = normalizeName(rawName);
|
||||
if (!nm) continue;
|
||||
if (nm === normalizedTarget || nm.includes(normalizedTarget) || normalizedTarget.includes(nm)) {
|
||||
// detect if this stop is a terminus (isLast / is_last / index == maxIdx)
|
||||
const isTerminus = Boolean((typeof s === 'object') && (s.isLast || s.is_last || s.isTerminus || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)));
|
||||
if (isTerminus) continue; // ne pas considérer les terminus
|
||||
if (isTerminus) continue;
|
||||
stopMatch = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!stopMatch) continue; // this horaire does not serve the target gare (or match failed)
|
||||
if (!stopMatch) continue;
|
||||
|
||||
// determine platform/quai for this stop/horaire
|
||||
const platform = (stopMatch && (stopMatch.quai || stopMatch.platform || stopMatch.quay)) || h.platform || h.quai || '';
|
||||
const normPlatform = String(platform || '').toLowerCase().replace(/\s+/g,'');
|
||||
|
||||
// if targetQuai is set, ensure it matches (loose compare: contains or equals)
|
||||
if (targetQuai) {
|
||||
if (!normPlatform) continue; // no platform info -> skip
|
||||
if (!normPlatform) continue;
|
||||
if (!(normPlatform === targetQuai || normPlatform.includes(targetQuai) || targetQuai.includes(normPlatform))) continue;
|
||||
}
|
||||
|
||||
// Ne garder que les horaires qui ont un départ au stop (on ne veut pas afficher les arrivées)
|
||||
const stopIsObject = stopMatch && typeof stopMatch === 'object';
|
||||
const hasDeparture = stopIsObject && (
|
||||
typeof stopMatch.heure_depart !== 'undefined' || typeof stopMatch.departure_time !== 'undefined' || typeof stopMatch.departure !== 'undefined' || typeof stopMatch.time !== 'undefined' || typeof stopMatch.departureTime !== 'undefined' || typeof stopMatch.horaire_time !== 'undefined'
|
||||
@@ -666,15 +620,12 @@ export default function ClassiquesDepartsQuais(){
|
||||
const hasArrivalOnly = stopIsObject && !hasDeparture && (
|
||||
typeof stopMatch.heure_arrivee !== 'undefined' || typeof stopMatch.arrival_time !== 'undefined' || typeof stopMatch.arrivee !== 'undefined' || typeof stopMatch.arrival !== 'undefined'
|
||||
);
|
||||
// si le stop correspond mais n'a que l'heure d'arrivée (pas de départ), ignorer cet horaire — on veut uniquement des départs
|
||||
if (stopIsObject && hasArrivalOnly) continue;
|
||||
|
||||
// pick time from stopMatch (si on a une heure de départ) ou fallback au niveau horaire
|
||||
const rawTime = (hasDeparture ? (stopMatch.heure_depart || stopMatch.time || stopMatch.heure || stopMatch.departure_time || stopMatch.departure || stopMatch.departureTime || stopMatch.horaire_time) : (h.heure_depart || h.time || h.heure || h.departure_time || h.departure || h.departureTime || h.horaire_time));
|
||||
const depHM = parseTimeHM(rawTime);
|
||||
if (depHM == null) continue;
|
||||
|
||||
// compute minutes until departure (allow next-day wrap)
|
||||
let minutesUntil = depHM - nowHM;
|
||||
if (minutesUntil < 0) minutesUntil = depHM + 24*60 - nowHM;
|
||||
|
||||
@@ -688,13 +639,15 @@ export default function ClassiquesDepartsQuais(){
|
||||
return;
|
||||
}
|
||||
|
||||
// split into today (circulatesToday AND depHM > nowHM) and tomorrow (circulatesTomorrow AND (depHM <= nowHM OR not circulatesToday))
|
||||
const todayCandidates = candidates.filter(c => c.circulatesToday && c.depHM > nowHM).sort((a,b) => a.depHM - b.depHM);
|
||||
const tomorrowCandidates = candidates.filter(c => c.circulatesTomorrow && (c.depHM <= nowHM || !c.circulatesToday)).sort((a,b) => a.depHM - b.depHM);
|
||||
|
||||
let chosen = null;
|
||||
if (todayCandidates.length > 0) chosen = todayCandidates[0];
|
||||
else if (tomorrowCandidates.length > 0) chosen = tomorrowCandidates[0];
|
||||
if (todayCandidates.length > 0) {
|
||||
chosen = { ...todayCandidates[0], isTomorrow: false };
|
||||
} else if (tomorrowCandidates.length > 0) {
|
||||
chosen = { ...tomorrowCandidates[0], isTomorrow: true };
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setCandidatesCount(candidates.length);
|
||||
@@ -712,232 +665,149 @@ export default function ClassiquesDepartsQuais(){
|
||||
return () => { mounted = false; };
|
||||
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
|
||||
|
||||
// toggle flag to alternate time/status when needed (observe selectedHoraire.status when present)
|
||||
const [showTime, setShowTime] = useState(true);
|
||||
// derive a simple status label string to use in the dependency array (avoids complex expressions)
|
||||
const _selectedStatusLabel = (() => {
|
||||
const horaireObj = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const computedLabel = horaireObj && horaireObj._status && horaireObj._status.label ? String(horaireObj._status.label) : null;
|
||||
const rawStatus = horaireObj && horaireObj.status ? (typeof horaireObj.status === 'string' ? horaireObj.status : (horaireObj.status && horaireObj.status.label ? String(horaireObj.status.label) : null)) : null;
|
||||
return String(computedLabel || rawStatus || mock.status || '').toLowerCase();
|
||||
// Rail is handled purely in CSS for better performance and alignment
|
||||
const showPlatform = (() => {
|
||||
const sched = selectedHoraire ? selectedHoraire.depHM : null;
|
||||
if (sched === null || !stationType) return true;
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
return nowHM >= (sched - threshold);
|
||||
})();
|
||||
|
||||
useEffect(()=>{
|
||||
let id = null;
|
||||
if (/retard|supprim/.test(_selectedStatusLabel)) {
|
||||
// ensure we start by showing the time
|
||||
setShowTime(true);
|
||||
id = setInterval(() => setShowTime(prev => !prev), 3000);
|
||||
} else {
|
||||
setShowTime(true);
|
||||
}
|
||||
return () => { if (id) clearInterval(id); };
|
||||
}, [_selectedStatusLabel, mock.status]);
|
||||
|
||||
// compute and position the rail so it covers from first stop center to last stop center
|
||||
useEffect(() => {
|
||||
function updateRail(){
|
||||
const rail = railRef.current;
|
||||
const list = stopsListRef.current;
|
||||
const right = rightRef.current;
|
||||
if(!rail || !list || !right) return;
|
||||
const items = Array.from(list.querySelectorAll('li'));
|
||||
if(items.length === 0) return;
|
||||
const first = items[0];
|
||||
// find destination (last with dest class) or fallback to last item
|
||||
const last = items.find(el => el.classList.contains('aff-quais__stop--strict-dest')) || items[items.length-1];
|
||||
|
||||
const rightRect = right.getBoundingClientRect();
|
||||
const firstRect = first.getBoundingClientRect();
|
||||
const lastRect = last.getBoundingClientRect();
|
||||
|
||||
const firstCenterY = firstRect.top + firstRect.height/4 - rightRect.top;
|
||||
const lastCenterY = lastRect.top + lastRect.height/2 - rightRect.top;
|
||||
|
||||
// add small padding so rail extends a bit beyond centers
|
||||
const pad = 9;
|
||||
const top = Math.max(0, firstCenterY - pad);
|
||||
const height = Math.max(8, (lastCenterY - firstCenterY) + pad*2);
|
||||
|
||||
// compute horizontal position so dots are centered next to the text
|
||||
const firstText = first.querySelector('.aff-quais__stop-text--strict') || first.querySelector('.aff-quais__stop-text');
|
||||
const textRect = firstText ? firstText.getBoundingClientRect() : firstRect;
|
||||
const centerX = (textRect.left - rightRect.left) - 28; // place rail left of text by ~20px
|
||||
const railWidth = 35;
|
||||
const left = centerX - railWidth/2;
|
||||
|
||||
// apply styles for the rail rectangle
|
||||
rail.style.top = `${Math.round(top)}px`;
|
||||
rail.style.height = `${Math.round(height)}px`;
|
||||
rail.style.left = `${Math.round(left)}px`;
|
||||
rail.style.width = `${railWidth}px`;
|
||||
// remove background-image reliance and render individual dots as DOM children
|
||||
// clear existing dots
|
||||
while (rail.firstChild) rail.removeChild(rail.firstChild);
|
||||
|
||||
// create a dot element for each item and position it exactly at the item's vertical center
|
||||
const railWidthComputed = parseInt(rail.style.width, 8) || railWidth;
|
||||
items.forEach(it => {
|
||||
const r = it.getBoundingClientRect();
|
||||
const centerY = r.top + r.height/2 - (rightRect.top + top);
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'aff-quais__rail-dot';
|
||||
dot.style.position = 'absolute';
|
||||
dot.style.left = '50%';
|
||||
dot.style.transform = 'translate(-50%, -50%)';
|
||||
dot.style.top = `${Math.round(centerY)}px`;
|
||||
// set dot diameter to rail width
|
||||
dot.style.width = `${railWidthComputed}px`;
|
||||
dot.style.height = `${railWidthComputed}px`;
|
||||
dot.style.borderRadius = '50%';
|
||||
dot.style.background = '#ffe600';
|
||||
dot.style.boxShadow = '0 0 0 4px rgba(255,230,102,0.06) inset';
|
||||
rail.appendChild(dot);
|
||||
});
|
||||
}
|
||||
|
||||
updateRail();
|
||||
window.addEventListener('resize', updateRail);
|
||||
const obs = new MutationObserver(updateRail);
|
||||
const listNode = stopsListRef.current;
|
||||
if(listNode) obs.observe(listNode, { childList:true, subtree:true });
|
||||
return ()=>{
|
||||
window.removeEventListener('resize', updateRail);
|
||||
if(obs) obs.disconnect();
|
||||
};
|
||||
}, [selectedHoraire]);
|
||||
// Observer changement des stops via selectedHoraire
|
||||
// Re-run rail effect lorsque selectedHoraire change (les stops affichés changent)
|
||||
|
||||
const displayTimeOrStatus = () => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const statusStrFromField = horaire ? (horaire.status || mock.status) : mock.status;
|
||||
const statusObj = horaire && horaire._status ? horaire._status : null;
|
||||
// prefer precomputed _displayTime if available
|
||||
const time = selectedHoraire && selectedHoraire._displayTime ? selectedHoraire._displayTime : (horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time);
|
||||
|
||||
// build canonical status label when we have computed status object
|
||||
let canonicalStatus = null;
|
||||
if (statusObj) {
|
||||
if (typeof statusObj.delayMinutes === 'number' && !Number.isNaN(statusObj.delayMinutes)) {
|
||||
canonicalStatus = `retardé +${Math.round(statusObj.delayMinutes)} min.`;
|
||||
} else if (statusObj.label && String(statusObj.label).toLowerCase().includes('supprim')) {
|
||||
canonicalStatus = 'supprimé';
|
||||
} else if (statusObj.label) {
|
||||
canonicalStatus = String(statusObj.label);
|
||||
}
|
||||
}
|
||||
|
||||
const s = (canonicalStatus || statusStrFromField || '').toLowerCase();
|
||||
const isAlert = s.includes('retard') || s.includes('supprim');
|
||||
const label = canonicalStatus || statusStrFromField || '';
|
||||
|
||||
// Logique de seuil d'affichage du quai
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
const sched = selectedHoraire && selectedHoraire.depHM ? selectedHoraire.depHM : null;
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
const showPlatform = (sched !== null) ? (nowHM >= (sched - threshold)) : true;
|
||||
|
||||
if (!showPlatform) {
|
||||
return (
|
||||
<span className="aff-quais__time-status-wrapper" aria-live="polite">
|
||||
<span className="aff-quais__time aff-quais__time--strict visible">--h--</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Always render both time and status inside a fixed-size wrapper to avoid layout shift
|
||||
if (!loadingHoraire && !selectedHoraire) {
|
||||
return (
|
||||
<span className="aff-quais__time-status-wrapper" aria-live="polite">
|
||||
<span className={`aff-quais__time aff-quais__time--strict ${showTime ? 'visible' : 'hidden'}`}>{time}</span>
|
||||
<span className={`aff-quais__time-status aff-quais__time-status--strict aff-quais__status--alert ${(!showTime && isAlert) ? 'visible' : 'hidden'}`}>{label}</span>
|
||||
</span>
|
||||
<div className="aff-quais-departs-redesign__container aff-quais-departs-redesign__empty">
|
||||
<div className="aff-quais-departs-redesign__empty-text">Aucun Départ Prévu</div>
|
||||
<div className="aff-quais-departs-redesign__footer">
|
||||
<div className="aff-quais-departs-redesign__footer-left"></div>
|
||||
<div className="aff-quais-departs-redesign__footer-right">
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="aff-quais__container aff-quais__container--strict aff-quais__container--depart">
|
||||
<div className="aff-quais__left aff-quais__left--strict">
|
||||
{/* logo (depuis le type de train) */}
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType;
|
||||
return <img src={getTypeLogo(trainType, typeLogos)} alt={trainType || 'train'} className="aff-quais__type-logo--strict" />;
|
||||
})()}
|
||||
return (
|
||||
<div className="aff-quais-departs-redesign__container">
|
||||
{/* LEFT PANEL: White Background */}
|
||||
<div className="aff-quais-departs-redesign__left">
|
||||
{/* SNCF Logo */}
|
||||
<img src="/type/quais/logo-sncf-color.svg" alt="SNCF" className="aff-quais-departs-redesign__sncf-logo" />
|
||||
|
||||
<div className="aff-quais__watermark" aria-hidden>départ</div>
|
||||
{/* affichage des états de chargement / erreur pour debug */}
|
||||
{horaireError ? <div className="aff-quais__error" style={{color:'crimson',fontSize:'0.9rem',marginTop:6}}>{horaireError}</div> : null}
|
||||
{loadingHoraire ? <div className="aff-quais__loading" style={{color:'#666',fontSize:'0.85rem',marginTop:6}}>Chargement...</div> : null}
|
||||
{debugEnabled ? (
|
||||
<div style={{marginTop:8, fontSize:'0.78rem', color:'#333'}}>
|
||||
<div>Candidates: {candidatesCount}</div>
|
||||
{selectedHoraire ? <pre style={{maxHeight:120,overflow:'auto',background:'#f7f7f7',padding:6}}>{JSON.stringify(selectedHoraire, null, 2)}</pre> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{/* logo du type en haut à droite */}
|
||||
<div className="aff-quais__row aff-quais__row--time-status">
|
||||
<div className="aff-quais-departs-redesign__time-status">
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const status = horaire ? (horaire.status || (horaire._status && horaire._status.label) || mock.status) : mock.status;
|
||||
const time = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time;
|
||||
const isAlert = (status || '').toLowerCase().includes('retard') || (status || '').toLowerCase().includes('supprim');
|
||||
if (isAlert) return displayTimeOrStatus();
|
||||
const statusObj = horaire && horaire._status ? horaire._status : null;
|
||||
const isSuppressed = statusObj && statusObj.label === 'supprimé';
|
||||
const isDelayed = statusObj && typeof statusObj.delayMinutes === 'number';
|
||||
const delayMinutes = statusObj ? statusObj.delayMinutes : null;
|
||||
|
||||
let timeText = horaire ? formatTimeNoSeconds(selectedHoraire.rawTime || horaire.heure_depart || horaire.time || horaire.heure) : mock.time;
|
||||
let statusText = horaire ? (horaire.status || (statusObj && statusObj.label) || mock.status) : mock.status;
|
||||
|
||||
if (showPlatform) {
|
||||
if (isSuppressed) {
|
||||
if (!alternateCancelPhase) {
|
||||
timeText = 'supprimé';
|
||||
statusText = '';
|
||||
} else {
|
||||
statusText = 'supprimé';
|
||||
}
|
||||
} else if (isDelayed) {
|
||||
if (!alternateDelayPhase) {
|
||||
timeText = `retardé +${delayMinutes} min`;
|
||||
statusText = '';
|
||||
} else {
|
||||
statusText = `retardé +${delayMinutes} min`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timeText = '--h--';
|
||||
statusText = '';
|
||||
}
|
||||
|
||||
const isTimeSuppressed = isSuppressed && !alternateCancelPhase;
|
||||
const isTimeDelayed = isDelayed && !alternateDelayPhase;
|
||||
|
||||
const isLongText = timeText.length > 5;
|
||||
let timeClass = '';
|
||||
if (isLongText) timeClass += ' aff-quais-departs-redesign__time--long-text';
|
||||
if (isTimeSuppressed) timeClass += ' aff-quais-departs-redesign__time--suppressed';
|
||||
if (isTimeDelayed) timeClass += ' aff-quais-departs-redesign__time--delayed';
|
||||
|
||||
let statusClass = '';
|
||||
if (isSuppressed) statusClass += ' aff-quais-departs-redesign__status--suppressed';
|
||||
if (isDelayed) statusClass += ' aff-quais-departs-redesign__status--delayed';
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="aff-quais__time aff-quais__time--strict">{time}</span>
|
||||
<span className="aff-quais__time-status aff-quais__time-status--strict">{status}</span>
|
||||
<span className={`aff-quais-departs-redesign__time${timeClass}`}>{timeText}</span>
|
||||
{statusText && <span className={`aff-quais-departs-redesign__status${statusClass}`}>{String(statusText).toLowerCase()}</span>}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="aff-quais__destination aff-quais__destination--strict">
|
||||
|
||||
<div className="aff-quais-departs-redesign__destination">
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const stop = selectedHoraire && selectedHoraire.stop ? selectedHoraire.stop : null;
|
||||
// destinationBig is the final destination of the train (gare_arrivee_*). The second line should be the station name of the stop on the route (if present) — avoid duplicating final destination.
|
||||
const destinationBig = horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig;
|
||||
const rawDestExpr = (stop ? ((typeof stop === 'string') ? stop : (stop.gare_nom || stop.nom || stop.name || stop.stop_name || '')) : (horaire ? (horaire.destination_station || '') : mock.destinationStation));
|
||||
return (
|
||||
<>
|
||||
<div className="aff-quais__destination-line1 aff-quais__destination-line1--strict">{destinationBig}</div>
|
||||
|
||||
</>
|
||||
);
|
||||
return showPlatform ? (horaire ? (horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '') : mock.destinationBig) : '--';
|
||||
})()}
|
||||
</div>
|
||||
<div className="aff-quais__train-id aff-quais__train-id--strict">{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType;
|
||||
const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : mock.trainNumber;
|
||||
return (<>{trainType} <span className="aff-quais__train-number--strict">{trainNumber}</span></>);
|
||||
})()}</div>
|
||||
|
||||
<div className="aff-quais-departs-redesign__train-info">
|
||||
{(() => {
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
const trainType = horaire ? (horaire.trainType || horaire.train_type || horaire.type_train || horaire.op || horaire.operator) : mock.trainType;
|
||||
const trainNumber = horaire ? (horaire.numero_train || horaire.numero || horaire.number || '') : mock.trainNumber;
|
||||
return showPlatform ? (
|
||||
<>
|
||||
<span className="aff-quais-departs-redesign__train-type">{trainType}</span>
|
||||
<span className="aff-quais-departs-redesign__train-number">{trainNumber}</span>
|
||||
</>
|
||||
) : '';
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Accessibility Icons */}
|
||||
<div className="aff-quais-departs-redesign__icons">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="4" r="1.5" fill="#7e859b" />
|
||||
<path d="M9 13a4 4 0 0 0 4 4h3" />
|
||||
<path d="M12 7.5v6" />
|
||||
<path d="M8 20a5 5 0 1 1 5-5" />
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: '12px' }}>
|
||||
<circle cx="5.5" cy="17.5" r="3.5" />
|
||||
<circle cx="18.5" cy="17.5" r="3.5" />
|
||||
<path d="M15 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-3 11.5V14l-3-3 4-3 2 3h2" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Watermark */}
|
||||
<div className="aff-quais-departs-redesign__watermark">départ</div>
|
||||
</div>
|
||||
<div className="aff-quais__separator--strict" aria-hidden></div>
|
||||
<div className="aff-quais__right aff-quais__right--strict" ref={rightRef}>
|
||||
{/* barre verticale de points (élément explicite pour fiabilité) */}
|
||||
<div className="aff-quais__stops-rail" aria-hidden ref={railRef} />
|
||||
<ul className="aff-quais__stops-list--strict" ref={stopsListRef}>
|
||||
|
||||
{/* RIGHT PANEL: Blue Background with Stops */}
|
||||
<div className="aff-quais-departs-redesign__right">
|
||||
<ul className="aff-quais-departs-redesign__stops-list">
|
||||
{(() => {
|
||||
if (!showPlatform) return null;
|
||||
const horaire = selectedHoraire && selectedHoraire.horaire ? selectedHoraire.horaire : null;
|
||||
// utiliser la helper getStops pour supporter plusieurs formats
|
||||
let stops = horaire ? getStops(horaire) : [];
|
||||
if (!Array.isArray(stops) || stops.length === 0) {
|
||||
// fallback parsing si la colonne est une chaîne JSON
|
||||
if (horaire && typeof horaire.stops === 'string') {
|
||||
try { const p = JSON.parse(horaire.stops); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
|
||||
|
||||
// construire une liste propre en extrayant un label pour chaque stop
|
||||
const labeled = stops.map((stop, idx) => {
|
||||
let label = '';
|
||||
if (typeof stop === 'string') label = stop;
|
||||
else if (stop && typeof stop === 'object') {
|
||||
// vérifier plusieurs clés possibles
|
||||
label = stop.gare_nom || stop.nom || stop.name || stop.stop_name || stop.stopName || stop.station_name || stop.station || stop.label || stop.stop || stop.name_gare || (stop.properties && (stop.properties.name || stop.properties.stop_name)) || '';
|
||||
// si l'objet possède une clé 'stop' qui est elle-même un objet
|
||||
if (!label && stop.stop && typeof stop.stop === 'object') {
|
||||
label = stop.stop.name || stop.stop.stop_name || '';
|
||||
}
|
||||
@@ -948,7 +818,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
|
||||
if (labeled.length === 0) return null;
|
||||
|
||||
// trouver l'index du stop correspondant à la gare demandée (selectedHoraire.stop)
|
||||
let matchedIndex = -1;
|
||||
try {
|
||||
if (selectedHoraire && selectedHoraire.stop) {
|
||||
@@ -957,7 +826,6 @@ export default function ClassiquesDepartsQuais(){
|
||||
const targetId = (typeof target === 'object') ? (target.gare_id || target.gareId || target.id || target.stop_id || target.stopId || null) : null;
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const s = stops[i];
|
||||
// compare by id first if available
|
||||
if (targetId != null) {
|
||||
const sid = (s && typeof s === 'object') ? (s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null) : null;
|
||||
if (sid != null && String(sid) === String(targetId)) { matchedIndex = i; break; }
|
||||
@@ -968,11 +836,8 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// filtrer pour ne garder que les prochains arrêts après la gare renseignée
|
||||
let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx > matchedIndex) : true);
|
||||
// trier par idx pour être sûr
|
||||
let subsequent = labeled.filter(it => (matchedIndex >= 0) ? (it.idx >= matchedIndex) : true);
|
||||
subsequent = subsequent.sort((a,b) => a.idx - b.idx);
|
||||
// Remove consecutive duplicate labels (case-insensitive)
|
||||
if (subsequent.length > 1) {
|
||||
const collapsed = [];
|
||||
for (const it of subsequent) {
|
||||
@@ -985,28 +850,45 @@ export default function ClassiquesDepartsQuais(){
|
||||
}
|
||||
subsequent = collapsed;
|
||||
}
|
||||
// si aucun arrêt après (ou pas de match) on peut retourner null ou le fallback; ici on renvoie le fallback si tout échoue
|
||||
if (subsequent.length === 0) return null;
|
||||
|
||||
return subsequent.map((item, mapIndex) => {
|
||||
const isLast = item.idx === (stops.length - 1);
|
||||
const isLast = mapIndex === subsequent.length - 1;
|
||||
const stop = item.original || {};
|
||||
const stopId = stop.gare_id || stop.gareId || stop.id || stop.stop_id || stop.stopId || null;
|
||||
const stopStatus = computeStatusForHoraireAtStop(horaire, stopId, item.label, perturbMap, serviceDate);
|
||||
const isStopSuppressed = stopStatus && stopStatus.label === 'supprimé';
|
||||
const isStopDelayed = stopStatus && typeof stopStatus.delayMinutes === 'number';
|
||||
const stopDelayMin = stopStatus ? stopStatus.delayMinutes : null;
|
||||
|
||||
let stopTextClass = '';
|
||||
if (isStopSuppressed) stopTextClass += ' aff-quais-departs-redesign__stop-text--suppressed';
|
||||
if (isStopDelayed) stopTextClass += ' aff-quais-departs-redesign__stop-text--delayed';
|
||||
|
||||
return (
|
||||
<li key={`${item.label}-${item.idx}-${mapIndex}`} className={isLast ? 'aff-quais__stop--strict aff-quais__stop--strict-dest' : 'aff-quais__stop--strict'}>
|
||||
<span className="aff-quais__stop-bullet--strict" />
|
||||
<span className="aff-quais__stop-text--strict">{item.label}</span>
|
||||
<li key={`${item.label}-${item.idx}-${mapIndex}`} className={`aff-quais-departs-redesign__stop ${isLast ? 'aff-quais-departs-redesign__stop--dest' : ''}`}>
|
||||
<span className="aff-quais-departs-redesign__stop-bullet" />
|
||||
<span className={`aff-quais-departs-redesign__stop-text${stopTextClass}`}>
|
||||
{item.label}
|
||||
{isStopSuppressed && <span className="aff-quais-departs-redesign__stop-status-badge aff-quais-departs-redesign__stop-status-badge--suppressed"> (non desservi)</span>}
|
||||
{isStopDelayed && <span className="aff-quais-departs-redesign__stop-status-badge aff-quais-departs-redesign__stop-status-badge--delayed"> (+{stopDelayMin} min)</span>}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</ul>
|
||||
</div>
|
||||
{/* footer band same as classic departures with clock on the right */}
|
||||
<div className="aff-quais__footer" aria-hidden>
|
||||
<div className="aff-quais__footer-inner">
|
||||
<div className="aff-quais__footer-left"></div>
|
||||
<div className="aff-quais__footer-right"><Clock /></div>
|
||||
|
||||
{/* BOTTOM FOOTER: Full Width */}
|
||||
<div className="aff-quais-departs-redesign__footer">
|
||||
<div className="aff-quais-departs-redesign__footer-left">
|
||||
ain.
|
||||
</div>
|
||||
<div className="aff-quais-departs-redesign__footer-right">
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const horaire_date = "2026-06-14";
|
||||
const baseDate = new Date(horaire_date);
|
||||
console.log("baseDate before setHours:", baseDate.toString());
|
||||
const sched = 330; // 05:30
|
||||
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
|
||||
console.log("baseDate after setHours:", baseDate.toString());
|
||||
|
||||
const currentTime = new Date("2026-06-14T04:49:15+02:00");
|
||||
console.log("currentTime:", currentTime.toString());
|
||||
|
||||
const diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
|
||||
console.log("diffMinutes:", diffMinutes);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const gareDisplay = "Ouges";
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
console.log("flexNeedle:", flexNeedle);
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
|
||||
console.log("gdata:", gdata);
|
||||
let fetchedType = null;
|
||||
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
|
||||
console.log("fetchedType:", fetchedType);
|
||||
}
|
||||
|
||||
run();
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('gares').select('id,nom,type').ilike('nom', '%Ouges%');
|
||||
console.log("Matched gares:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: horaires, error: hErr } = await supabase.from('horaires').select('*').limit(2);
|
||||
console.log("Horaires sample:", horaires);
|
||||
const { data: perturbs, error: pErr } = await supabase.from('perturbations').select('*').limit(2);
|
||||
console.log("Perturbations sample:", perturbs);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,85 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
const normalizeName = (s) => {
|
||||
if (!s) return '';
|
||||
try {
|
||||
const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
|
||||
} catch (e) {
|
||||
return String(s).toLowerCase().trim();
|
||||
}
|
||||
};
|
||||
|
||||
const parseTimeHM = (v) => {
|
||||
if (!v) return null;
|
||||
const m = String(v).match(/(\d{1,2}):(\d{2})/);
|
||||
if (m) return parseInt(m[1], 10)*60 + parseInt(m[2], 10);
|
||||
return null;
|
||||
};
|
||||
|
||||
async function run() {
|
||||
const gareDisplay = "Ouges";
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
|
||||
const stationType = gdata && gdata.length ? gdata[0].type.toLowerCase() : null;
|
||||
console.log("stationType:", stationType);
|
||||
|
||||
const thresholdForStationType = (typeGare) => {
|
||||
const tG = String(typeGare || '').trim().toLowerCase();
|
||||
if (tG === 'interurbaine' || tG.includes('interurb')) return 360;
|
||||
if (tG === 'ville' || tG.includes('ville')) return 30;
|
||||
return 30;
|
||||
};
|
||||
const threshold = thresholdForStationType(stationType);
|
||||
console.log("threshold:", threshold);
|
||||
|
||||
const { data: allData } = await supabase.from('horaires').select('*').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
const h = horaires[0];
|
||||
|
||||
const stops = h.gares_desservies;
|
||||
const targetStationName = gareDisplay;
|
||||
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
|
||||
}) || null;
|
||||
|
||||
h._displayPlatform = (stopForStation && stopForStation.quai_reel) || h.quai_reel || '';
|
||||
console.log("_displayPlatform:", h._displayPlatform);
|
||||
|
||||
const platform = (h._displayPlatform || '').toString().trim();
|
||||
let showPlatform = Boolean(platform);
|
||||
console.log("Boolean(platform):", showPlatform);
|
||||
|
||||
if (showPlatform) {
|
||||
let diffMinutes = null;
|
||||
h._depHM = parseTimeHM(h.heure_depart);
|
||||
const sched = h._depHM;
|
||||
console.log("sched:", sched);
|
||||
|
||||
const currentTime = new Date();
|
||||
if (sched !== null) {
|
||||
const baseDate = new Date();
|
||||
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
|
||||
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
|
||||
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;
|
||||
}
|
||||
console.log("diffMinutes:", diffMinutes);
|
||||
|
||||
if (diffMinutes !== null) {
|
||||
showPlatform = (diffMinutes <= threshold);
|
||||
} else {
|
||||
showPlatform = false;
|
||||
}
|
||||
}
|
||||
console.log("FINAL showPlatform:", showPlatform);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires, error } = await supabase.from('horaires').select('*').eq('numero_train', '891807');
|
||||
console.log(horaires[0]);
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,16 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires } = await supabase.from('horaires').select('*').limit(100);
|
||||
const matched = horaires.filter(h => JSON.stringify(h).includes("Ouges"));
|
||||
const h = matched[0];
|
||||
console.log("Train:", h.numero_train);
|
||||
console.log("Stops:", h.gares_desservies.map(s => ({
|
||||
gare_nom: s.gare_nom,
|
||||
quai_reel: s.quai_reel,
|
||||
gare_id: s.gare_id
|
||||
})));
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('perturbations').select('*');
|
||||
console.log("Perturbations:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
async function test() {
|
||||
const { data: horaires, error } = await supabase.from('horaires_departs').select('*');
|
||||
console.log('Error:', error);
|
||||
}
|
||||
test();
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('horaires').select('numero_train, heure_depart, circule_dimanche').ilike('gares_desservies', '%Ouges%');
|
||||
console.log("Trains Ouges on Sunday:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data } = await supabase.from('horaires').select('numero_train, circule_dimanche').in('numero_train', ['891801', '891813']);
|
||||
console.log("Trains Ouges on Sunday:", data);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,11 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
console.log("Trains Ouges on Sunday:", horaires.length);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,14 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
horaires.forEach(h => {
|
||||
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
|
||||
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,14 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
|
||||
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
|
||||
|
||||
async function run() {
|
||||
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
|
||||
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
|
||||
horaires.forEach(h => {
|
||||
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
|
||||
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart} with quai_reel: ${stop ? stop.quai_reel : 'none'}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in new issue
Block a user