V1.1.0 //

-> Refonte du design de sélection d'afficheurs
-> Ajout du Mode PiP
-> Patch du bug du logo de l'horaire pas affiché. Corrigé pour les Horaires TER à 6 numéros.
This commit is contained in:
AdminGit committed 2026-10-07 15:32:21 +02:00
1 parent c30a0e9002
commit 6cc1e1820d
14 files changed
+1143 -849

No files matched your search

+64 -99
View File
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { WcsCard, WcsCardBody, WcsButton, WcsBadge } from 'wcs-react';
import './App.css';
import { applyPolyfills, defineCustomElements } from 'wcs-core/loader';
@@ -24,11 +25,9 @@ import { getSourceFromPath } from './services/railwayService';
applyPolyfills().then(() => defineCustomElements(window));
// Petit routeur client sans dépendances externes
function useSimpleRouter() {
function normalizePath(p) {
if (!p) return '/';
// retirer query string et hash
p = String(p).split(/[?#]/)[0];
if (p.indexOf('index.html') !== -1) return '/';
p = p.trim();
@@ -74,7 +73,6 @@ function App() {
const prefix = source === 'supabase' ? '/ferrovia' : '';
const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/');
// Composant Home moderne & responsive pour Mobile, Tablette et Desktop
function Home({ navigate, prefix, source }) {
const POPULAR_HOME_GARES = [
'Paris-Gare-de-Lyon',
@@ -88,118 +86,85 @@ function App() {
];
return (
<div className="launcher-container">
{/* Carte de bienvenue */}
<div className="launcher-card">
<div className="launcher-header">
<div>
<h1 className="launcher-title">🚅 Téléaffichage Ferroviaire SNCF</h1>
<p className="launcher-subtitle">
{source === 'supabase'
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
</p>
<div className="launcher-container" style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
<WcsCard mode="raised">
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
<div>
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚅 Téléaffichage Ferroviaire SNCF</h1>
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
{source === 'supabase'
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
: "Simulateur d'écrans de gare officiels connectés en direct aux flux GTFS & Open Data SNCF."}
</p>
</div>
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
</WcsBadge>
</div>
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
</span>
</div>
{/* Cartes principales : Départs / Arrivées */}
<div className="launcher-grid launcher-grid--2col">
<div
className="launcher-select-card"
role="button"
tabIndex={0}
onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
navigate(`${prefix}/departs`);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
<div style={{ display: 'flex', gap: '24px', flexDirection: 'row', flexWrap: 'wrap', marginBottom: '24px' }}>
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer', border: '1px solid var(--wcs-light)' }} mode="flat" onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
navigate(`${prefix}/departs`);
}
}}
>
<div>
<span className="launcher-card-icon">🚆</span>
<span className="launcher-card-tag launcher-card-tag--modern">Départs</span>
<h2 className="launcher-card-title">Prochains Départs</h2>
<p className="launcher-card-desc">
Consultez les trains au départ, leurs voies, retards et dessertes complètes.
</p>
</div>
<div style={{ marginTop: 12 }}>
<span className="launcher-btn launcher-btn--primary">
Consulter les Départs ›
</span>
</div>
</div>
}}>
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '32px' }}>🚆</span>
<WcsBadge className="wcs-success">Départs</WcsBadge>
</div>
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochains Départs</h2>
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Consultez les trains au départ, leurs voies, retards et dessertes complètes.</p>
<WcsButton className="wcs-primary" style={{ width: '100%' }}>Consulter les Départs</WcsButton>
</WcsCardBody>
</WcsCard>
<div
className="launcher-select-card"
role="button"
tabIndex={0}
onClick={() => {
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
navigate(`${prefix}/arrivees`);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
<WcsCard style={{ flex: '1 1 300px', cursor: 'pointer', border: '1px solid var(--wcs-light)' }} mode="flat" onClick={() => {
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
navigate(`${prefix}/arrivees`);
}
}}
>
<div>
<span className="launcher-card-icon">🏁</span>
<span className="launcher-card-tag launcher-card-tag--modern">Arrivées</span>
<h2 className="launcher-card-title">Prochaines Arrivées</h2>
<p className="launcher-card-desc">
Suivez en direct les trains attendus en gare, provenances et voies assignées.
</p>
</div>
<div style={{ marginTop: 12 }}>
<span className="launcher-btn launcher-btn--secondary">
Consulter les Arrivées ›
</span>
}}>
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '32px' }}>🏁</span>
<WcsBadge className="wcs-info">Arrivées</WcsBadge>
</div>
<h2 style={{ fontSize: '18px', marginTop: '16px' }}>Prochaines Arrivées</h2>
<p style={{ color: 'var(--wcs-text-light)', minHeight: '60px' }}>Suivez en direct les trains attendus en gare, provenances et voies assignées.</p>
<WcsButton className="wcs-secondary" style={{ width: '100%' }}>Consulter les Arrivées</WcsButton>
</WcsCardBody>
</WcsCard>
</div>
<div style={{ marginBottom: '24px' }}>
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct par gare :</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{POPULAR_HOME_GARES.map((gName) => (
<WcsButton
key={gName}
mode="stroked"
shape="round"
size="s"
onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
}}
>
📍 {gName.replace(/-/g, ' ')}
</WcsButton>
))}
</div>
</div>
</div>
{/* Raccourcis rapides vers les gares */}
<div className="launcher-chips-container" style={{ marginTop: '8px' }}>
<span className="launcher-chips-label">Accès direct par gare :</span>
<div className="launcher-chips-list">
{POPULAR_HOME_GARES.map((gName) => (
<button
key={gName}
type="button"
className="launcher-chip"
onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
navigate(`${prefix}/afficheurs/${encodeURIComponent(gName)}?mode=departs`);
}}
>
📍 {gName.replace(/-/g, ' ')}
</button>
))}
</div>
</div>
{/* Présentation des types d'afficheurs */}
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginTop: '20px' }}>
<div style={{ fontSize: '13px', color: '#5f6368', lineHeight: 1.6 }}>
<hr style={{ border: 'none', borderTop: '1px solid var(--wcs-light)', margin: '24px 0' }} />
<div style={{ fontSize: '14px', color: 'var(--wcs-text-light)', lineHeight: 1.6 }}>
✨ <strong>Formats disponibles :</strong> Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie.
</div>
</div>
</div>
</WcsCardBody>
</WcsCard>
</div>
);
}
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
if (
cleanRoute.startsWith('/afficheurs/classiques/flash-info') ||
cleanRoute.startsWith('/afficheurs/classiques/flash-circulation') ||
+141 -21
View File
@@ -1,23 +1,24 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import './AfficheurViewportWrapper.css';
import { createPortal } from 'react-dom';
/**
* AfficheurViewportWrapper
* Assure un affichage 16:9 strict (1920x1080) sur tous les appareils
* (Mobile portrait, Mobile paysage, Tablettes, Ordinateurs, TV).
* Ajoute automatiquement des bordures noires (letterboxing / pillarboxing)
* pour conserver fidèlement les proportions du design original sans distorsion.
*/
export default function AfficheurViewportWrapper({ children }) {
const [scale, setScale] = useState(1);
const [pipWindow, setPipWindow] = useState(null);
const containerRef = useRef(null);
const [isPipSupported, setIsPipSupported] = useState(false);
const [autoPipTriggered, setAutoPipTriggered] = useState(false);
const isMobile = typeof window !== 'undefined' && (window.innerWidth <= 768 || /Mobi|Android/i.test(navigator.userAgent));
useEffect(() => {
if ('documentPictureInPicture' in window) {
setIsPipSupported(true);
}
const calculateScale = () => {
// Dimensions de la fenêtre d'affichage (support viewport iOS / Android safe area)
const w = window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 1920;
const h = window.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 1080;
// Échelle basée sur la résolution standard 1920x1080 (16:9)
const scaleX = w / 1920;
const scaleY = h / 1080;
const s = Math.min(scaleX, scaleY);
@@ -27,7 +28,6 @@ export default function AfficheurViewportWrapper({ children }) {
calculateScale();
window.addEventListener('resize', calculateScale);
window.addEventListener('orientationchange', calculateScale);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', calculateScale);
}
@@ -41,16 +41,136 @@ export default function AfficheurViewportWrapper({ children }) {
};
}, []);
const togglePiP = async () => {
if (pipWindow) {
pipWindow.close();
return;
}
if ('documentPictureInPicture' in window) {
try {
const pipW = isMobile ? window.innerWidth * 0.8 : 1920 / 3;
const pipH = isMobile ? (window.innerWidth * 0.8) * (1080 / 1920) : 1080 / 3;
const pip = await window.documentPictureInPicture.requestWindow({
width: Math.max(pipW, 300),
height: Math.max(pipH, 170),
});
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
pip.document.head.appendChild(style);
} catch(e) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href;
pip.document.head.appendChild(link);
}
});
pip.document.body.classList.add('pip-mode');
pip.document.body.style.margin = '0';
pip.document.body.style.padding = '0';
pip.document.body.style.overflow = 'hidden';
pip.document.body.style.width = '100vw';
pip.document.body.style.height = '100vh';
pip.document.body.style.backgroundColor = '#000';
pip.document.body.style.display = 'flex';
pip.document.body.style.justifyContent = 'center';
pip.document.body.style.alignItems = 'center';
const updatePipScale = () => {
const w = pip.innerWidth;
const h = pip.innerHeight;
const pipScale = Math.min(w / 1920, h / 1080);
pip.document.body.style.setProperty('--pip-scale', pipScale);
};
updatePipScale();
pip.addEventListener('resize', updatePipScale);
pip.addEventListener("pagehide", () => {
setPipWindow(null);
});
setPipWindow(pip);
setAutoPipTriggered(true);
} catch (err) {
console.error('Failed to enter PiP mode', err);
}
}
};
const content = (
<div
className="afficheur-pwa-canvas"
style={{
transform: pipWindow ? 'scale(1)' : `scale(${scale})`,
width: '1920px',
height: '1080px',
position: 'relative'
}}
>
{children}
</div>
);
return (
<div className="afficheur-pwa-screen-wrapper">
<div
className="afficheur-pwa-canvas"
style={{
transform: `scale(${scale})`,
}}
>
{children}
</div>
<div className="afficheur-pwa-screen-wrapper" ref={containerRef} style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
{!pipWindow && content}
{isMobile && isPipSupported && !pipWindow && !autoPipTriggered && (
<div
onClick={togglePiP}
style={{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.85)',
zIndex: 10000,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
cursor: 'pointer',
padding: '20px',
textAlign: 'center'
}}
>
<span style={{ fontSize: '48px', marginBottom: '16px' }}>📱</span>
<h2 style={{ margin: '0 0 16px 0' }}>Appuyez pour lancer l'afficheur</h2>
<p style={{ opacity: 0.8 }}>Le mode Picture-in-Picture s'activera pour une meilleure expérience sur mobile.</p>
</div>
)}
{pipWindow && (
<div style={{ color: 'white', textAlign: 'center', padding: '24px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
<h2>L'afficheur est en cours de lecture dans le lecteur réduit (PiP)</h2>
<p style={{ opacity: 0.7 }}>Vous pouvez naviguer sur votre téléphone, l'afficheur restera au premier plan.</p>
<button onClick={() => pipWindow.close()} style={{ padding: '12px 24px', marginTop: '24px', background: '#00439e', color: 'white', border: 'none', borderRadius: '4px', fontSize: '16px', fontWeight: 'bold' }}>
Fermer le mode PiP
</button>
</div>
)}
{pipWindow && createPortal(
<div style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: `scale(var(--pip-scale, 1))`
}}>
{content}
</div>,
pipWindow.document.body
)}
</div>
);
}
+11
View File
@@ -15,3 +15,14 @@ root.render(
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
// Register PWA Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
+98 -125
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react';
import WcsButton from '../components/WcsButton';
import { WcsCard, WcsCardBody, WcsButton, WcsInput, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
@@ -90,139 +90,112 @@ export default function Arrivees({ onBack, navigate }) {
}
};
const steps = [
{ text: 'Choix de la gare', button: { kind: 'Text', text: '1' }, complete: false },
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, disable: true },
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
];
return (
<div className="launcher-container">
{/* Stepper */}
<div className="launcher-stepper">
<div className="launcher-step launcher-step--active">
<span className="launcher-step-num">1</span>
<span>Choix de la gare</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step">
<span className="launcher-step-num">2</span>
<span>Type d'afficheur</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step">
<span className="launcher-step-num">3</span>
<span>Options & Affichage</span>
</div>
</div>
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
<WcsHorizontalStepper steps={steps} currentStep={0} style={{ marginBottom: '24px' }} />
<div className="launcher-card">
{/* Header */}
<div className="launcher-header">
<div>
<h1 className="launcher-title">🏁 Prochaines Arrivées</h1>
<p className="launcher-subtitle">Recherchez une gare pour afficher le tableau des arrivées en temps réel.</p>
<WcsCard mode="raised">
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🏁 Prochaines Arrivées</h1>
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>Recherchez une gare pour afficher le tableau des arrivées en temps réel.</p>
</div>
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
</WcsBadge>
</div>
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
</span>
</div>
{/* Barre de recherche */}
<div className="launcher-search-box">
<input
type="search"
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="launcher-search-input"
aria-label="Rechercher une gare"
autoFocus
/>
{search && (
<button
type="button"
className="launcher-search-clear"
onClick={() => setSearch('')}
aria-label="Effacer la recherche"
>
✕
</button>
)}
</div>
{/* Suggestions rapides / Gares populaires */}
<div className="launcher-chips-container">
<span className="launcher-chips-label">Gares fréquentes :</span>
<div className="launcher-chips-list">
{POPULAR_GARES.map((gName) => (
<button
key={gName}
type="button"
className="launcher-chip"
onClick={() => handleSelectGare(gName)}
>
{gName.replace(/-/g, ' ')}
</button>
))}
<div style={{ marginBottom: '24px' }}>
<WcsInput
type="text"
icon="search"
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
value={search}
onWcsInput={(e) => setSearch(e.target.value)}
style={{ width: '100%' }}
clearable={true}
/>
</div>
</div>
{loading && <p style={{ color: '#00439e', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
{error && <p style={{ color: '#d70022', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '24px 16px', color: '#666', background: '#f8fafc', borderRadius: '8px', border: '1px dashed #d0d7de' }}>
<p style={{ margin: 0, fontSize: '14px' }}>
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '24px', color: '#777', background: '#fff', borderRadius: '8px' }}>
Aucune gare trouvée pour « <strong>{search}</strong> ».
</div>
)}
{/* Liste des résultats */}
{displayedGares.length > 0 && (
<div className="launcher-results-list" role="list">
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div
key={g.uic || nom}
className="launcher-result-item"
onClick={() => handleSelectGare(nom)}
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
tabIndex={0}
role="listitem"
<div style={{ marginBottom: '24px' }}>
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Gares fréquentes :</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{POPULAR_GARES.map((gName) => (
<WcsButton
key={gName}
mode="stroked"
shape="round"
size="s"
onClick={() => handleSelectGare(gName)}
>
<div>
<div className="launcher-result-name">{nom}</div>
{g.commune && (
<div className="launcher-result-sub">
{g.commune} {g.departement ? `(${g.departement})` : ''}
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <span className="launcher-result-tag">UIC {g.uic}</span>}
<span style={{ color: '#00439e', fontSize: '18px', fontWeight: 'bold' }}>›</span>
</div>
</div>
);
})}
{gName.replace(/-/g, ' ')}
</WcsButton>
))}
</div>
</div>
)}
{/* Navigation retour */}
<div className="launcher-footer-nav">
<WcsButton
mode="plain"
onClick={onBack}
label="← Retour à l'accueil"
/>
</div>
</div>
{loading && <p style={{ color: 'var(--wcs-primary)', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
{error && <p style={{ color: 'var(--wcs-danger)', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '24px 16px', color: 'var(--wcs-text-light)', background: 'var(--wcs-light)', borderRadius: '8px', border: '1px dashed var(--wcs-gray)' }}>
<p style={{ margin: 0, fontSize: '14px' }}>
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
</p>
</div>
)}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '24px', color: 'var(--wcs-text-light)', background: 'var(--wcs-white)', borderRadius: '8px' }}>
Aucune gare trouvée pour « <strong>{search}</strong> ».
</div>
)}
{displayedGares.length > 0 && (
<div role="list" style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<WcsCard
key={g.uic || nom}
mode="flat"
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
onClick={() => handleSelectGare(nom)}
>
<WcsCardBody style={{ padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', color: 'var(--wcs-primary)' }}>{nom}</div>
{g.commune && (
<div style={{ fontSize: '13px', color: 'var(--wcs-text-light)' }}>
{g.commune} {g.departement ? `(${g.departement})` : ''}
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <WcsBadge>{g.uic}</WcsBadge>}
<span style={{ color: 'var(--wcs-primary)', fontSize: '20px', fontWeight: 'bold' }}>›</span>
</div>
</WcsCardBody>
</WcsCard>
);
})}
</div>
)}
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
<WcsButton mode="clear" onClick={onBack}>
← Retour à l'accueil
</WcsButton>
</div>
</WcsCardBody>
</WcsCard>
</div>
);
}
+98 -125
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react';
import WcsButton from '../components/WcsButton';
import { WcsCard, WcsCardBody, WcsButton, WcsInput, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
@@ -90,139 +90,112 @@ export default function Departs({ onBack, navigate }) {
}
};
const steps = [
{ text: 'Choix de la gare', button: { kind: 'Text', text: '1' }, complete: false },
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, disable: true },
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
];
return (
<div className="launcher-container">
{/* Stepper */}
<div className="launcher-stepper">
<div className="launcher-step launcher-step--active">
<span className="launcher-step-num">1</span>
<span>Choix de la gare</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step">
<span className="launcher-step-num">2</span>
<span>Type d'afficheur</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step">
<span className="launcher-step-num">3</span>
<span>Options & Affichage</span>
</div>
</div>
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
<WcsHorizontalStepper steps={steps} currentStep={0} style={{ marginBottom: '24px' }} />
<div className="launcher-card">
{/* Header */}
<div className="launcher-header">
<div>
<h1 className="launcher-title">🚆 Prochains Départs</h1>
<p className="launcher-subtitle">Recherchez une gare pour afficher le tableau des départs en temps réel.</p>
<WcsCard mode="raised">
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>🚆 Prochains Départs</h1>
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>Recherchez une gare pour afficher le tableau des départs en temps réel.</p>
</div>
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
</WcsBadge>
</div>
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
</span>
</div>
{/* Barre de recherche */}
<div className="launcher-search-box">
<input
type="search"
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="launcher-search-input"
aria-label="Rechercher une gare"
autoFocus
/>
{search && (
<button
type="button"
className="launcher-search-clear"
onClick={() => setSearch('')}
aria-label="Effacer la recherche"
>
✕
</button>
)}
</div>
{/* Suggestions rapides / Gares populaires */}
<div className="launcher-chips-container">
<span className="launcher-chips-label">Gares fréquentes :</span>
<div className="launcher-chips-list">
{POPULAR_GARES.map((gName) => (
<button
key={gName}
type="button"
className="launcher-chip"
onClick={() => handleSelectGare(gName)}
>
{gName.replace(/-/g, ' ')}
</button>
))}
<div style={{ marginBottom: '24px' }}>
<WcsInput
type="text"
icon="search"
placeholder="Rechercher une gare (ex: Dijon, Paris, Lyon, Besançon...)"
value={search}
onWcsInput={(e) => setSearch(e.target.value)}
style={{ width: '100%' }}
clearable={true}
/>
</div>
</div>
{loading && <p style={{ color: '#00439e', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
{error && <p style={{ color: '#d70022', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '24px 16px', color: '#666', background: '#f8fafc', borderRadius: '8px', border: '1px dashed #d0d7de' }}>
<p style={{ margin: 0, fontSize: '14px' }}>
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '24px', color: '#777', background: '#fff', borderRadius: '8px' }}>
Aucune gare trouvée pour « <strong>{search}</strong> ».
</div>
)}
{/* Liste des résultats */}
{displayedGares.length > 0 && (
<div className="launcher-results-list" role="list">
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div
key={g.uic || nom}
className="launcher-result-item"
onClick={() => handleSelectGare(nom)}
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
tabIndex={0}
role="listitem"
<div style={{ marginBottom: '24px' }}>
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Gares fréquentes :</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{POPULAR_GARES.map((gName) => (
<WcsButton
key={gName}
mode="stroked"
shape="round"
size="s"
onClick={() => handleSelectGare(gName)}
>
<div>
<div className="launcher-result-name">{nom}</div>
{g.commune && (
<div className="launcher-result-sub">
{g.commune} {g.departement ? `(${g.departement})` : ''}
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <span className="launcher-result-tag">UIC {g.uic}</span>}
<span style={{ color: '#00439e', fontSize: '18px', fontWeight: 'bold' }}>›</span>
</div>
</div>
);
})}
{gName.replace(/-/g, ' ')}
</WcsButton>
))}
</div>
</div>
)}
{/* Navigation retour */}
<div className="launcher-footer-nav">
<WcsButton
mode="plain"
onClick={onBack}
label="← Retour à l'accueil"
/>
</div>
</div>
{loading && <p style={{ color: 'var(--wcs-primary)', fontWeight: 600 }}>⏳ Chargement des gares...</p>}
{error && <p style={{ color: 'var(--wcs-danger)', fontWeight: 600 }}>⚠️ Erreur : {error}</p>}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '24px 16px', color: 'var(--wcs-text-light)', background: 'var(--wcs-light)', borderRadius: '8px', border: '1px dashed var(--wcs-gray)' }}>
<p style={{ margin: 0, fontSize: '14px' }}>
💡 Saisissez au moins <strong>2 lettres</strong> pour chercher parmi plus de 3 500 gares du réseau français.
</p>
</div>
)}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '24px', color: 'var(--wcs-text-light)', background: 'var(--wcs-white)', borderRadius: '8px' }}>
Aucune gare trouvée pour « <strong>{search}</strong> ».
</div>
)}
{displayedGares.length > 0 && (
<div role="list" style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<WcsCard
key={g.uic || nom}
mode="flat"
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
onClick={() => handleSelectGare(nom)}
>
<WcsCardBody style={{ padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 'bold', fontSize: '16px', color: 'var(--wcs-primary)' }}>{nom}</div>
{g.commune && (
<div style={{ fontSize: '13px', color: 'var(--wcs-text-light)' }}>
{g.commune} {g.departement ? `(${g.departement})` : ''}
</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{g.uic && <WcsBadge>{g.uic}</WcsBadge>}
<span style={{ color: 'var(--wcs-primary)', fontSize: '20px', fontWeight: 'bold' }}>›</span>
</div>
</WcsCardBody>
</WcsCard>
);
})}
</div>
)}
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
<WcsButton mode="clear" onClick={onBack}>
← Retour à l'accueil
</WcsButton>
</div>
</WcsCardBody>
</WcsCard>
</div>
);
}
+63 -87
View File
@@ -1,5 +1,5 @@
import React from 'react';
import WcsButton from '../components/WcsButton';
import { WcsCard, WcsCardBody, WcsButton, WcsBadge, WcsHorizontalStepper } from 'wcs-react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath } from '../services/railwayService';
@@ -21,6 +21,7 @@ export default function Displays({ onBack, navigate }) {
id: 'classiques',
label: 'Classiques',
sublabel: 'Téléaffichage Historique',
sublabel: 'Téléaffichage Historique',
tag: 'Rétro / Vintage',
tagType: 'retro',
icon: '📺',
@@ -33,7 +34,7 @@ export default function Displays({ onBack, navigate }) {
tag: 'Gares Connectées',
tagType: 'modern',
icon: '📱',
desc: 'Écrans d\'information enrichis avec multimédia, infogare et dessertes complètes.'
desc: "Écrans d'information enrichis avec multimédia, infogare et dessertes complètes."
},
{
id: 'transilien',
@@ -46,8 +47,7 @@ export default function Displays({ onBack, navigate }) {
}
];
// Nettoyage du chemin pour extraire la gare même avec /ferrovia
const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/^\/ferrovia/, '');
const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/ferrovia/, '');
const parts = cleanPath.split('/').filter(Boolean);
const typeIds = types.map(t => t.id);
let gare = null;
@@ -63,14 +63,13 @@ export default function Displays({ onBack, navigate }) {
const g = paramsForGare.get('gare');
if (g) {
try {
gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' ');
gare = decodeURIComponent(String(g).replace(/\\+/g, ' ')).replace(/-/g, ' ');
} catch (e) {
gare = String(g).replace(/-/g, ' ');
}
}
}
// Déterminer le mode (departs/arrivees)
const params = new URLSearchParams(window.location.search || '');
let mode = params.get('mode');
if (!mode) {
@@ -83,96 +82,73 @@ export default function Displays({ onBack, navigate }) {
const formattedGareName = gare ? gare.replace(/-/g, ' ') : 'Non sélectionnée';
const steps = [
{ text: formattedGareName, button: { kind: 'Icon', iconName: 'check' }, complete: true },
{ text: "Type d'afficheur", button: { kind: 'Text', text: '2' }, complete: false },
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, disable: true }
];
return (
<div className="launcher-container">
{/* Stepper */}
<div className="launcher-stepper">
<div className="launcher-step launcher-step--completed">
<span className="launcher-step-num">✓</span>
<span>{formattedGareName}</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step launcher-step--active">
<span className="launcher-step-num">2</span>
<span>Type d'afficheur</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step">
<span className="launcher-step-num">3</span>
<span>Options & Affichage</span>
</div>
</div>
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px', overflowY: 'auto', maxHeight: 'calc(100vh - 120px)' }}>
<WcsHorizontalStepper steps={steps} currentStep={1} style={{ marginBottom: '24px' }} />
<div className="launcher-card">
{/* Header */}
<div className="launcher-header">
<div>
<h1 className="launcher-title">Sélectionnez le style d'afficheur</h1>
<p className="launcher-subtitle">
Gare : <strong style={{ color: '#002257' }}>{formattedGareName}</strong> • Mode : <strong style={{ color: '#00439e' }}>{mode === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong>
</p>
</div>
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
</span>
</div>
{/* Grille de sélection tactile des styles d'afficheurs */}
<div className="launcher-grid launcher-grid--4col">
{types.map((t) => (
<div
key={t.id}
className="launcher-select-card"
role="button"
tabIndex={0}
onClick={() => {
if (typeof navigate === 'function') {
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
}
}}
>
<div>
<span className="launcher-card-icon">{t.icon}</span>
<span className={`launcher-card-tag ${t.tagType === 'retro' ? 'launcher-card-tag--retro' : 'launcher-card-tag--modern'}`}>
{t.tag}
</span>
<h2 className="launcher-card-title">{t.label}</h2>
<div style={{ fontSize: '13px', fontWeight: 600, color: '#00439e', marginBottom: '8px' }}>{t.sublabel}</div>
<p className="launcher-card-desc">{t.desc}</p>
</div>
<div style={{ marginTop: 12 }}>
<span className="launcher-btn launcher-btn--secondary" style={{ padding: '8px 12px', fontSize: '14px' }}>
Choisir ce style ›
</span>
</div>
<WcsCard mode="raised">
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
<div>
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>Sélectionnez le style d'afficheur</h1>
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
Gare : <strong>{formattedGareName}</strong> • Mode : <strong>{mode === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong>
</p>
</div>
))}
</div>
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
</WcsBadge>
</div>
{/* Navigation retour */}
<div className="launcher-footer-nav">
<WcsButton
mode="plain"
onClick={() => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
{types.map((t) => (
<WcsCard
key={t.id}
mode="flat"
style={{ cursor: 'pointer', border: '1px solid var(--wcs-light)' }}
onClick={() => {
if (typeof navigate === 'function') {
const kebab = gare ? String(gare).trim().replace(/\\s+/g, '-').replace(/\\/g, '/') : '';
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
}
}}
>
<WcsCardBody style={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '32px', marginRight: '8px' }}>{t.icon}</span>
<WcsBadge className={t.tagType === 'retro' ? 'wcs-secondary' : 'wcs-success'}>{t.tag}</WcsBadge>
</div>
<h2 style={{ fontSize: '18px', margin: '0 0 4px 0', color: 'var(--wcs-primary)' }}>{t.label}</h2>
<div style={{ fontSize: '13px', fontWeight: 'bold', color: 'var(--wcs-primary)', marginBottom: '8px' }}>{t.sublabel}</div>
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', flexGrow: 1, marginBottom: '16px' }}>{t.desc}</p>
<WcsButton className="wcs-secondary" style={{ width: '100%', marginTop: 'auto' }}>
Choisir ce style
</WcsButton>
</WcsCardBody>
</WcsCard>
))}
</div>
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
<WcsButton mode="clear" onClick={() => {
if (typeof onBack === 'function') {
onBack();
} else if (navigate) {
navigate(`${prefix}/${mode}`);
}
}}
label="← Changer de gare"
/>
</div>
</div>
}}>
← Changer de gare
</WcsButton>
</div>
</WcsCardBody>
</WcsCard>
</div>
);
}
+186 -375
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import WcsButton from '../components/WcsButton';
import { WcsCard, WcsCardBody, WcsButton, WcsBadge, WcsCheckbox, WcsSelect, WcsSelectOption, WcsInput, WcsHorizontalStepper } from 'wcs-react';
import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService';
export default function SelectAfficheur({ onBack, navigate }) {
@@ -16,7 +16,6 @@ export default function SelectAfficheur({ onBack, navigate }) {
const [manualPlatform, setManualPlatform] = useState('');
const [loading, setLoading] = useState(true);
// État des pages sélectionnées pour les afficheurs AFL
const [aflPages, setAflPages] = useState({
horaires: true,
infos: false,
@@ -25,8 +24,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
corresp: false
});
// Options spécifiques pour les afficheurs Classiques
const [extendedLines, setExtendedLines] = useState(0); // 0, 1, 2, 3
const [extendedLines, setExtendedLines] = useState(0);
const [showTime, setShowTime] = useState(false);
const toggleAflPage = (key) => {
@@ -43,9 +41,8 @@ export default function SelectAfficheur({ onBack, navigate }) {
});
};
// Decode and format station name for display
const decodedGare = gareParam
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
? decodeURIComponent(String(gareParam).replace(/\\+/g, ' ')).replace(/-/g, ' ')
: '';
useEffect(() => {
@@ -129,398 +126,212 @@ export default function SelectAfficheur({ onBack, navigate }) {
}
};
const steps = [
{ text: decodedGare || 'Gare', button: { kind: 'Icon', iconName: 'check' }, complete: true },
{ text: typeParam.toUpperCase(), button: { kind: 'Icon', iconName: 'check' }, complete: true },
{ text: 'Options & Affichage', button: { kind: 'Text', text: '3' }, complete: false }
];
return (
<div className="launcher-container">
{/* Stepper */}
<div className="launcher-stepper">
<div className="launcher-step launcher-step--completed">
<span className="launcher-step-num">✓</span>
<span>{decodedGare || 'Gare'}</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step launcher-step--completed">
<span className="launcher-step-num">✓</span>
<span>{typeParam.toUpperCase()}</span>
</div>
<span className="launcher-step-separator">›</span>
<div className="launcher-step launcher-step--active">
<span className="launcher-step-num">3</span>
<span>Options & Affichage</span>
</div>
</div>
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px' }}>
<WcsHorizontalStepper steps={steps} currentStep={2} style={{ marginBottom: '24px' }} />
<div className="launcher-card">
{/* Header */}
<div className="launcher-header">
<div>
<h1 className="launcher-title">Configuration de l'afficheur</h1>
<p className="launcher-subtitle">
Gare : <strong style={{ color: '#002257' }}>{decodedGare}</strong> • Mode : <strong style={{ color: '#00439e' }}>{modeParam === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong> • Style : <strong style={{ color: '#008b47' }}>{typeParam.toUpperCase()}</strong>
</p>
</div>
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
</span>
</div>
{/* Grille responsive 2 colonnes (1 col sur mobile) */}
<div className="launcher-grid launcher-grid--2col">
{/* Option 1 : Afficheur Général */}
<div className="launcher-select-card" style={{ cursor: 'default' }}>
<WcsCard mode="raised">
<WcsCardBody>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
<div>
<span className="launcher-card-icon">🖥️</span>
<span className="launcher-card-tag launcher-card-tag--modern">Écran Général</span>
<h2 className="launcher-card-title">Afficheur Général (Hall)</h2>
<p className="launcher-card-desc">
Affiche la liste complète des circulations avec basculement automatique des pages.
<h1 style={{ margin: '0 0 8px 0', fontSize: '24px', color: 'var(--wcs-primary)' }}>Configuration de l'afficheur</h1>
<p style={{ margin: 0, color: 'var(--wcs-text-light)' }}>
Gare : <strong>{decodedGare}</strong> • Mode : <strong>{modeParam === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'}</strong> • Style : <strong>{typeParam.toUpperCase()}</strong>
</p>
{typeParam === 'afl' && (
<div className="launcher-checklist-box">
<div className="launcher-checklist-header">
<span className="launcher-checklist-title">
Pages incluses dans la boucle :
</span>
<div className="launcher-checklist-actions">
<button
type="button"
onClick={() => selectAllAflPages(true)}
className="launcher-link-btn"
>
Tout cocher
</button>
<span style={{ color: '#ccc' }}>|</span>
<button
type="button"
onClick={() => selectAllAflPages(false)}
className="launcher-link-btn"
style={{ color: '#666' }}
>
Horaires seuls
</button>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<label className="launcher-checkbox-label">
<input
type="checkbox"
checked={aflPages.horaires}
onChange={() => toggleAflPage('horaires')}
className="launcher-checkbox"
/>
<span><strong>Page 1 & 2</strong> (Horaires des trains)</span>
</label>
<label className="launcher-checkbox-label">
<input
type="checkbox"
checked={aflPages.infos}
onChange={() => toggleAflPage('infos')}
className="launcher-checkbox"
/>
<span><strong>Informations</strong> (Trafic & Travaux)</span>
</label>
<label className="launcher-checkbox-label">
<input
type="checkbox"
checked={aflPages.securite1}
onChange={() => toggleAflPage('securite1')}
className="launcher-checkbox"
/>
<span><strong>Sécurité 1</strong> (Bagages délaissés)</span>
</label>
<label className="launcher-checkbox-label">
<input
type="checkbox"
checked={aflPages.securite2}
onChange={() => toggleAflPage('securite2')}
className="launcher-checkbox"
/>
<span><strong>Sécurité 2</strong> (Vigipirate / 3117)</span>
</label>
<label className="launcher-checkbox-label">
<input
type="checkbox"
checked={aflPages.corresp}
onChange={() => toggleAflPage('corresp')}
className="launcher-checkbox"
/>
<span><strong>Correspondances</strong> (Bus & Tram)</span>
</label>
</div>
</div>
)}
{typeParam === 'classiques' && (
<div className="launcher-checklist-box">
<div className="launcher-checklist-header">
<span className="launcher-checklist-title">
Options d'affichage Classiques :
</span>
</div>
{/* Choix du nombre de lignes étendues */}
<div style={{ marginBottom: '14px' }}>
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '8px' }}>
Lignes étendues (Dessertes défilantes) :
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}>
{[0, 1, 2, 3].map((num) => (
<button
key={num}
type="button"
onClick={() => setExtendedLines(num)}
style={{
padding: '8px 4px',
borderRadius: '8px',
border: extendedLines === num ? '2px solid #00439e' : '1px solid #c2dbfe',
background: extendedLines === num ? '#00439e' : '#ffffff',
color: extendedLines === num ? '#ffffff' : '#002257',
fontWeight: 700,
fontSize: '13px',
cursor: 'pointer',
transition: 'all 0.15s ease'
}}
>
{num === 0 ? '0 (Standard)' : `${num} étendue${num > 1 ? 's' : ''}`}
</button>
))}
</div>
<div style={{ fontSize: '12px', color: '#555', marginTop: '6px' }}>
{extendedLines > 0
? `Les ${extendedLines} premier${extendedLines > 1 ? 's' : ''} train${extendedLines > 1 ? 's' : ''} s'afficheront sur 2 lignes réduites avec le défilement de toutes les gares.`
: 'Affichage classique compact sans défilement de gares.'}
</div>
</div>
{/* Option Afficher l'heure */}
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '10px' }}>
<label className="launcher-checkbox-label" style={{ cursor: 'pointer' }}>
<input
type="checkbox"
checked={showTime}
onChange={(e) => setShowTime(e.target.checked)}
className="launcher-checkbox"
/>
<span>
<strong>Afficher l'heure & informations</strong>
</span>
</label>
<div style={{ fontSize: '12px', color: '#555', marginLeft: '28px', marginTop: '2px' }}>
Réserve la 10e ligne pour l'horloge numérique [ HH MM SS ] et un bandeau d'information défilant (horaires sur 9 lignes max).
</div>
</div>
</div>
)}
</div>
<button
type="button"
onClick={handleGeneral}
className="launcher-btn launcher-btn--primary"
>
🖥️ Lancer l'Afficheur Général
</button>
<WcsBadge className={source === 'supabase' ? 'wcs-secondary' : 'wcs-primary'} shape="rounded">
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
</WcsBadge>
</div>
{/* Option 2 : Afficheur par Quai */}
<div className="launcher-select-card" style={{ cursor: 'default' }}>
<div>
<span className="launcher-card-icon">🚉</span>
<span className="launcher-card-tag launcher-card-tag--retro">Écran de Voie</span>
<h2 className="launcher-card-title">Afficheur par Quai</h2>
<p className="launcher-card-desc">
Affiche le prochain train au départ/arrivée avec la liste déroulante des gares desservies sur la voie.
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '24px', marginBottom: '24px' }}>
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '24px', marginRight: '8px' }}>🖥️</span>
<WcsBadge className="wcs-info">Écran Général</WcsBadge>
</div>
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Afficheur Général (Hall)</h2>
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px' }}>
Affiche la liste complète des circulations avec basculement automatique des pages.
</p>
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
{loading ? (
<div style={{ fontSize: '14px', color: '#00439e', fontWeight: 600 }}>⏳ Détection des voies réelles...</div>
) : (
<div>
{platforms.length > 0 ? (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '6px' }}>
Voies actives détectées :
</label>
<select
value={selectedPlatform}
onChange={(e) => {
setSelectedPlatform(e.target.value);
setManualPlatform('');
}}
style={{
width: '100%',
padding: '12px',
borderRadius: '8px',
border: '2px solid #c2dbfe',
fontSize: '15px',
fontWeight: 600,
background: '#ffffff',
color: '#002257'
}}
>
{platforms.map(p => (
<option key={p} value={p}>Voie {p}</option>
))}
</select>
{typeParam === 'afl' && (
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<span style={{ fontWeight: 'bold' }}>Pages incluses dans la boucle :</span>
<div>
<WcsButton mode="clear" size="s" onClick={() => selectAllAflPages(true)}>Tout cocher</WcsButton>
<WcsButton mode="clear" size="s" onClick={() => selectAllAflPages(false)}>Horaires seuls</WcsButton>
</div>
) : (
<div style={{ fontSize: '13px', color: '#666', marginBottom: '16px', background: '#fff', padding: '8px 12px', borderRadius: '6px' }}>
ℹ️ Saisie manuelle de la voie ci-dessous.
</div>
)}
<div>
<label style={{ display: 'block', fontSize: '13px', fontWeight: 700, color: '#002257', marginBottom: '6px' }}>
Ou saisie libre d'une voie :
</label>
<input
type="text"
placeholder="Ex: A, B, 1, 2 bis..."
value={manualPlatform}
onChange={(e) => setManualPlatform(e.target.value)}
style={{
width: '100%',
padding: '12px',
borderRadius: '8px',
border: '2px solid #e2ebf6',
fontSize: '15px',
boxSizing: 'border-box',
background: '#ffffff'
}}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<WcsCheckbox checked={aflPages.horaires} onWcsChange={() => toggleAflPage('horaires')}><strong>Page 1 & 2</strong> (Horaires)</WcsCheckbox>
<WcsCheckbox checked={aflPages.infos} onWcsChange={() => toggleAflPage('infos')}><strong>Informations</strong> (Trafic)</WcsCheckbox>
<WcsCheckbox checked={aflPages.securite1} onWcsChange={() => toggleAflPage('securite1')}><strong>Sécurité 1</strong> (Bagages)</WcsCheckbox>
<WcsCheckbox checked={aflPages.securite2} onWcsChange={() => toggleAflPage('securite2')}><strong>Sécurité 2</strong> (Vigipirate)</WcsCheckbox>
<WcsCheckbox checked={aflPages.corresp} onWcsChange={() => toggleAflPage('corresp')}><strong>Correspondances</strong></WcsCheckbox>
</div>
</div>
)}
</div>
</div>
<button
type="button"
onClick={handlePlatform}
className="launcher-btn launcher-btn--success"
>
🚉 Lancer l'Afficheur de Quai
</button>
{typeParam === 'classiques' && (
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Options d'affichage :</p>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontSize: '14px', marginBottom: '8px' }}>Lignes étendues :</label>
<div style={{ display: 'flex', gap: '8px' }}>
{[0, 1, 2, 3].map(num => (
<WcsButton
key={num}
mode={extendedLines === num ? 'solid' : 'stroked'}
size="s"
onClick={() => setExtendedLines(num)}
>
{num === 0 ? '0' : num}
</WcsButton>
))}
</div>
</div>
<WcsCheckbox checked={showTime} onWcsChange={(e) => setShowTime(e.detail.checked)}><strong>Afficher l'heure & infos</strong></WcsCheckbox>
</div>
)}
<WcsButton className="wcs-primary" style={{ width: '100%', marginTop: 'auto' }} onClick={handleGeneral}>
Lancer l'Afficheur Général
</WcsButton>
</WcsCardBody>
</WcsCard>
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '24px', marginRight: '8px' }}>🚉</span>
<WcsBadge className="wcs-warning">Écran de Voie</WcsBadge>
</div>
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Afficheur par Quai</h2>
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px' }}>
Affiche le prochain train au départ/arrivée avec la liste déroulante des gares desservies sur la voie.
</p>
<div style={{ background: 'var(--wcs-light)', padding: '16px', borderRadius: '4px', marginBottom: '16px', flexGrow: 1 }}>
{loading ? (
<p style={{ fontWeight: 'bold', color: 'var(--wcs-primary)' }}>⏳ Détection des voies...</p>
) : (
<>
{platforms.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', fontWeight: 'bold', marginBottom: '8px', fontSize: '14px' }}>Voies actives détectées :</label>
<WcsSelect
value={selectedPlatform}
onWcsChange={(e) => {
setSelectedPlatform(e.detail.value);
setManualPlatform('');
}}
>
{platforms.map(p => (
<WcsSelectOption key={p} value={p}>Voie {p}</WcsSelectOption>
))}
</WcsSelect>
</div>
)}
<div>
<label style={{ display: 'block', fontWeight: 'bold', marginBottom: '8px', fontSize: '14px' }}>Saisie libre d'une voie :</label>
<WcsInput
placeholder="Ex: A, B, 1, 2 bis..."
value={manualPlatform}
onWcsInput={(e) => setManualPlatform(e.target.value)}
/>
</div>
</>
)}
</div>
<WcsButton className="wcs-success" style={{ width: '100%', marginTop: 'auto' }} onClick={handlePlatform}>
Lancer l'Afficheur de Quai
</WcsButton>
</WcsCardBody>
</WcsCard>
{typeParam === 'classiques' && (
<>
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '24px', marginRight: '8px' }}>⚡</span>
<WcsBadge className="wcs-warning">Flash Info Trafic</WcsBadge>
</div>
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Flash Information</h2>
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px', flexGrow: 1 }}>
Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
</p>
<WcsButton className="wcs-warning" style={{ width: '100%', marginTop: 'auto' }} onClick={() => {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
}}>
Lancer le Flash Information
</WcsButton>
</WcsCardBody>
</WcsCard>
<WcsCard mode="flat" style={{ border: '1px solid var(--wcs-light)' }}>
<WcsCardBody style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '24px', marginRight: '8px' }}>ℹ️</span>
<WcsBadge className="wcs-info">Messages Gare</WcsBadge>
</div>
<h2 style={{ fontSize: '20px', margin: '0 0 8px 0' }}>Informations Voyageurs</h2>
<p style={{ fontSize: '14px', color: 'var(--wcs-text-light)', marginBottom: '16px', flexGrow: 1 }}>
Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité).
</p>
<WcsButton className="wcs-info" style={{ width: '100%', marginTop: 'auto' }} onClick={() => {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
}}>
Lancer l'Afficheur Informations
</WcsButton>
</WcsCardBody>
</WcsCard>
</>
)}
</div>
{/* Options 3 & 4 pour le mode Classiques : Flash Info & Informations */}
{typeParam === 'classiques' && (
<>
{/* Option 3 : Flash Information */}
<div className="launcher-select-card" style={{ cursor: 'default' }}>
<div>
<span className="launcher-card-icon">⚡</span>
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#fab23e', color: '#00337f' }}>
Flash Info Trafic
</span>
<h2 className="launcher-card-title">Flash Information</h2>
<p className="launcher-card-desc">
Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
</p>
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
⚡ Conforme au design historique <em>Flash Circulation</em> avec fond ambre et bandeau bleu nuit.
</div>
</div>
</div>
<button
type="button"
onClick={() => {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
}}
className="launcher-btn"
style={{ background: '#00337f', color: '#fab23e', border: 'none', fontWeight: 700 }}
>
⚡ Lancer le Flash Information
</button>
{typeParam === 'afl' && (
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '24px', marginBottom: '24px' }}>
<p style={{ fontWeight: 'bold', marginBottom: '12px' }}>Accès direct à une page fixe AFL :</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('1')}>Page 1</WcsButton>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('2')}>Page 2</WcsButton>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('infos')}>Informations</WcsButton>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('securite1')}>Sécurité (Bagages)</WcsButton>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('securite2')}>Sécurité (Vigipirate)</WcsButton>
<WcsButton mode="stroked" size="s" onClick={() => handleSinglePage('corresp')}>Correspondances</WcsButton>
</div>
{/* Option 4 : Informations (Messages Dynamiques) */}
<div className="launcher-select-card" style={{ cursor: 'default' }}>
<div>
<span className="launcher-card-icon">ℹ️</span>
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#00337f', color: '#ffffff' }}>
Messages Gare
</span>
<h2 className="launcher-card-title">Informations Voyageurs</h2>
<p className="launcher-card-desc">
Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité en gare).
</p>
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
ℹ️ Conforme au design historique <em>Information</em> avec messages voyageurs de la gare de {decodedGare}.
</div>
</div>
</div>
<button
type="button"
onClick={() => {
const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
}}
className="launcher-btn"
style={{ background: '#00337f', color: '#ffffff', border: 'none', fontWeight: 700 }}
>
ℹ️ Lancer l'Afficheur Informations
</button>
</div>
</>
</div>
)}
</div>
{/* Accès direct aux pages fixes AFL */}
{typeParam === 'afl' && (
<div style={{ borderTop: '1px solid #e2ebf6', paddingTop: '20px', marginBottom: '20px' }}>
<span className="launcher-chips-label">Accès direct à une page fixe AFL :</span>
<div className="launcher-chips-list">
<button type="button" onClick={() => handleSinglePage('1')} className="launcher-chip">
Page 1 (Horaires)
</button>
<button type="button" onClick={() => handleSinglePage('2')} className="launcher-chip">
Page 2 (Horaires)
</button>
<button type="button" onClick={() => handleSinglePage('infos')} className="launcher-chip">
Informations Trafic
</button>
<button type="button" onClick={() => handleSinglePage('securite1')} className="launcher-chip">
Sécurité (Bagages)
</button>
<button type="button" onClick={() => handleSinglePage('securite2')} className="launcher-chip">
Sécurité (Vigipirate / 3117)
</button>
<button type="button" onClick={() => handleSinglePage('corresp')} className="launcher-chip">
Correspondances
</button>
</div>
</div>
)}
{/* Navigation retour */}
<div className="launcher-footer-nav">
<WcsButton
mode="plain"
onClick={() => {
<div style={{ borderTop: '1px solid var(--wcs-light)', paddingTop: '16px' }}>
<WcsButton mode="clear" onClick={() => {
if (typeof onBack === 'function') {
onBack();
} else if (typeof navigate === 'function') {
navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
}
}}
label="← Retour au choix du style"
/>
</div>
</div>
}}>
← Retour au choix du style
</WcsButton>
</div>
</WcsCardBody>
</WcsCard>
</div>
);
}
+11 -10
View File
@@ -11,14 +11,14 @@ const normalizeStr = (s) => {
// Table des logos issue de public/type/data.json
const LOGOS_DATA = [
{ file: "logo-aleop.svg", slug: "aleop", name: "Aleop" },
{ file: "logo-auvergerhonealpes.svg", slug: "auvergerhonealpes", name: "Auvergerhonealpes" },
{ file: "sncf-voyageurs-logo.png", slug: "auvergerhonealpes", name: "TER AURA" },
{ file: "logo-breizhgo.svg", slug: "breizhgo", name: "Breizhgo" },
{ file: "logo-cfl.svg", slug: "cfl", name: "CFL" },
{ file: "logo-db.svg", slug: "db", name: "DB" },
{ file: "logo-eurostar.svg", slug: "eurostar", name: "Eurostar" },
{ file: "logo-fluo.svg", slug: "fluo", name: "Fluo" },
{ file: "logo-frecciarossa.svg", slug: "frecciarossa", name: "Frecciarossa" },
{ file: "logo-hautsdefrance.svg", slug: "hautsdefrance", name: "Hautsdefrance" },
{ file: "logo-hautsdefrance.svg", slug: "hautsdefrance", name: "TER HDF" },
{ file: "logo-ice.svg", slug: "ice", name: "ICE" },
{ file: "logo-intercites.svg", slug: "intercites", name: "Intercités" },
{ file: "logo-ler.svg", slug: "ler", name: "Ler" },
@@ -43,7 +43,7 @@ const LOGOS_DATA = [
{ file: "logo-sncf1985.svg", slug: "sncf1985", name: "Sncf1985" },
{ file: "logo-sncf1992.svg", slug: "sncf1992", name: "Sncf1992" },
{ file: "logo-teoz.svg", slug: "teoz", name: "Teoz" },
{ file: "logo-ter.svg", slug: "ter", name: "TER" },
{ file: "logo-ter-rhone-alpes.svg", slug: "ter-rhone-alpes", name: "TER Rhone-Alpes" },
{ file: "logo-teralsace.svg", slug: "teralsace", name: "Ter Alsace" },
{ file: "logo-teraquitaine.svg", slug: "teraquitaine", name: "Ter Aquitaine" },
@@ -64,6 +64,7 @@ const LOGOS_DATA = [
{ file: "logo-tramtrain.svg", slug: "tramtrain", name: "Tramtrain" },
{ file: "logo-trenitalia.svg", slug: "trenitalia", name: "Trenitalia" },
{ file: "logo-zou.svg", slug: "zou", name: "Zou" }
];
const logoLookup = new Map();
@@ -85,7 +86,7 @@ LOGOS_DATA.forEach((item) => {
*/
export function getTrainLogoAndMeta(horaire) {
if (!horaire) {
return { logoSrc: '/type/logo-sncf.svg', typeLabel: 'TER', numTrain: '' };
return { logoSrc: '/type/sncf-voyageurs-logo.png', typeLabel: 'TER', numTrain: '' };
}
const opRaw = String(
@@ -152,7 +153,7 @@ export function getTrainLogoAndMeta(horaire) {
} else if (opNorm.includes('renfe') || opNorm.includes('ave')) {
matchedLogo = logoLookup.get('renfeave');
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
matchedLogo = logoLookup.get('trackcar') || { file: 'track-car.svg', name: 'Car TER' };
matchedLogo = logoLookup.get('trackcar') || { file: 'sncf-voyageurs-logo.png', name: 'Car TER' };
}
}
@@ -194,7 +195,7 @@ export function getTrainLogoAndMeta(horaire) {
} else if (opNorm.includes('lyria')) {
typeLabel = 'TGV Lyria';
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
typeLabel = 'AUTOCAR';
typeLabel = 'Car TER';
} else if (opNorm.includes('ter')) {
typeLabel = 'TER';
}
@@ -222,11 +223,11 @@ export function getTrainLogoAndMeta(horaire) {
} else if ((p3 >= 860 && p3 <= 862) || (p4 >= 1675 && p4 <= 1684)) {
matchedBrandSlug = 'remi'; matchedBrandLabel = 'RÉMI';
} else if ((p3 >= 863 && p3 <= 869) || (p4 >= 1685 && p4 <= 1709)) {
matchedBrandSlug = 'teraquitaine'; matchedBrandLabel = 'TER NOUVELLE-AQUITAINE';
matchedBrandSlug = 'teraquitaine'; matchedBrandLabel = 'TER NA';
} else if ((p3 >= 870 && p3 <= 872) || (p3 >= 876 && p3 <= 878) || (p4 >= 1710 && p4 <= 1719) || (p4 >= 1730 && p4 <= 1734)) {
matchedBrandSlug = 'lio'; matchedBrandLabel = 'liO';
} else if ((p3 >= 873 && p3 <= 875) || (p3 >= 882 && p3 <= 890) || (p4 >= 1720 && p4 <= 1729) || (p4 >= 1750 && p4 <= 1774) || (p4 >= 1790 && p4 <= 1799)) {
matchedBrandSlug = 'ter-rhone-alpes'; matchedBrandLabel = 'TER AUVERGNE-RHÔNE-ALPES';
matchedBrandSlug = 'ter-rhone-alpes'; matchedBrandLabel = 'TER AURA';
} else if ((p3 >= 879 && p3 <= 881) || (p4 >= 1735 && p4 <= 1749)) {
matchedBrandSlug = 'zou'; matchedBrandLabel = 'ZOU !';
} else if ((p3 >= 891 && p3 <= 895) || (p4 >= 1775 && p4 <= 1789)) {
@@ -235,13 +236,13 @@ export function getTrainLogoAndMeta(horaire) {
if (matchedBrandSlug && matchedBrandLabel) {
typeLabel = matchedBrandLabel;
if (!matchedLogo || matchedLogo.file === 'logo-ter.svg' || matchedLogo.file === 'logo-sncf.svg') {
if (!matchedLogo || matchedLogo.file === 'logo-sncf.svg' || matchedLogo.file === 'logo-sncf.svg') {
matchedLogo = logoLookup.get(matchedBrandSlug) || matchedLogo;
}
}
}
}
const logoFile = (matchedLogo && matchedLogo.file) || 'logo-sncf.svg';
const logoFile = (matchedLogo && matchedLogo.file) || 'sncf-voyageurs-logo.png';
const logoSrc = `/type/${logoFile}`;
return { logoSrc, typeLabel, numTrain };