From 983f4f0160d6b4f34d79fd75f5d74980ffe0eb4d Mon Sep 17 00:00:00 2001 From: mathis Date: Sun, 30 Aug 2026 22:07:34 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9sactivation=20des=20annonces=20sonores?= =?UTF-8?q?=20et=20refonte=20responsive=20mobile/tablette=20des=20pages=20?= =?UTF-8?q?de=20guidage=20WCS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 3 + src/App.js | 160 +++++--- src/hooks/useAnnonceSonoreRealtime.js | 150 +------ src/hooks/useAutoAnnonce.js | 94 +---- src/pages/Arrivees.js | 232 ++++++----- src/pages/Departs.js | 232 ++++++----- src/pages/Displays.js | 176 +++++--- src/pages/SelectAfficheur.js | 566 +++++++++++--------------- src/styles/Launcher.css | 544 +++++++++++++++++++++++++ 9 files changed, 1315 insertions(+), 842 deletions(-) create mode 100644 src/styles/Launcher.css diff --git a/src/App.css b/src/App.css index ca7b500..cacc089 100644 --- a/src/App.css +++ b/src/App.css @@ -8,6 +8,9 @@ @import "wcs-core/design-tokens/dist/sncf-reseau.css"; /* styles.scss SNCF GROUPE */ @import "wcs-core/design-tokens/dist/sncf-groupe.css"; + +/* Styles responsive pour les pages de guidage & sélection */ +@import "./styles/Launcher.css"; @font-face { font-family: Avenir; src: url("/public/fonts/avenir-lighter.woff"); diff --git a/src/App.js b/src/App.js index b978c54..079be8e 100644 --- a/src/App.js +++ b/src/App.js @@ -72,57 +72,125 @@ function App() { const prefix = source === 'supabase' ? '/ferrovia' : ''; const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/'); - // Composant Home explicite (panels cliquables) + // Composant Home moderne & responsive pour Mobile, Tablette et Desktop function Home({ navigate, prefix, source }) { - const panelStyle = { - width: '42%', - cursor: 'pointer', - padding: 24, - borderRadius: 10, - boxShadow: '0 4px 12px rgba(0,0,0,0.08)', - background: '#fff', - textAlign: 'center', - border: '1px solid #e0e8f6', - transition: 'transform 0.15s ease' - }; + const POPULAR_HOME_GARES = [ + 'Paris-Gare-de-Lyon', + 'Besançon-Viotte', + 'Dijon-Ville', + 'Lyon-Part-Dieu', + 'Marseille-Saint-Charles', + 'Lille-Flandres', + 'Strasbourg', + 'Bordeaux-Saint-Jean' + ]; return ( -
-
-
- {source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'} -
-

Accueil

-

- {source === 'supabase' - ? "Consultez les afficheurs basés sur les données de votre BDD Supabase." - : "Consultez les afficheurs connectés aux flux réels GTFS et Open Data SNCF."} -

-
- -
-
{ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); }} - onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); })() : null)} - style={panelStyle} - aria-label="Accéder aux départs" - > -

🚆 Départs

-

Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.

+
+ {/* Carte de bienvenue */} +
+
+
+

🚅 Téléaffichage Ferroviaire SNCF

+

+ {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."} +

+
+ + {source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'} +
-
{ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); }} - onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate(`${prefix}/arrivees`); })() : null)} - style={panelStyle} - aria-label="Accéder aux arrivées" - > -

🏁 Arrivées

-

Consultez les prochaines arrivées d'une gare en temps réel.

+ {/* Cartes principales : Départs / Arrivées */} +
+
{ + try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} + navigate(`${prefix}/departs`); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} + navigate(`${prefix}/departs`); + } + }} + > +
+ 🚆 + Départs +

Prochains Départs

+

+ Consultez les trains au départ, leurs voies, retards et dessertes complètes. +

+
+
+ + Consulter les Départs › + +
+
+ +
{ + try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} + navigate(`${prefix}/arrivees`); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} + navigate(`${prefix}/arrivees`); + } + }} + > +
+ 🏁 + Arrivées +

Prochaines Arrivées

+

+ Suivez en direct les trains attendus en gare, provenances et voies assignées. +

+
+
+ + Consulter les Arrivées › + +
+
+
+ + {/* Raccourcis rapides vers les gares */} +
+ Accès direct par gare : +
+ {POPULAR_HOME_GARES.map((gName) => ( + + ))} +
+
+ + {/* Présentation des types d'afficheurs */} +
+
+ ✨ Formats disponibles : Afficheurs Fluides & Lumineux (AFL), Téléaffichage historique (Classiques), Écrans Vidéo Voyageurs (EVA) et Écrans par Quai / Voie. +
diff --git a/src/hooks/useAnnonceSonoreRealtime.js b/src/hooks/useAnnonceSonoreRealtime.js index db612a8..3372873 100644 --- a/src/hooks/useAnnonceSonoreRealtime.js +++ b/src/hooks/useAnnonceSonoreRealtime.js @@ -1,148 +1,6 @@ -import { useEffect, useRef } from 'react'; -import supabase from '../lib/supabase'; - -// Helper pour normaliser le nom du quai pour comparaison robuste -const normalizeQuai = (val) => { - if (!val) return ''; - return String(val) - .toLowerCase() - .replace(/\s+/g, '') - .replace(/voie/g, '') - .replace(/quai/g, ''); -}; - +// Annonces sonores désactivées sur les afficheurs export default function useAnnonceSonoreRealtime(gareId, affType = null, quaiName = null) { - const audioRef = useRef(null); - const lastAnnonceIdRef = useRef(null); - - useEffect(() => { - if (!supabase || !gareId) return; - - console.debug(`[useAnnonceSonoreRealtime] Setup for gareId: ${gareId}, type: ${affType}, quai: ${quaiName}`); - - const playAnnonce = (newRecord) => { - // 1. Déterminer les afficheurs cibles - const variables = newRecord.annonce?.variables_values || {}; - const targetAfficheurs = variables.afficheurs || []; - const trainQuai = variables.quai || ''; - - // Si targetAfficheurs est renseigné, filtrer - if (affType && targetAfficheurs.length > 0) { - const isTargeted = targetAfficheurs.includes(affType); - if (!isTargeted) { - console.debug(`[useAnnonceSonoreRealtime] Ignoré : cet afficheur (${affType}) n'est pas ciblé par l'annonce.`); - return; - } - - // Si l'afficheur est de type "Quais" (se termine par "Quais"), on vérifie la voie/quai - const isQuaiDisplay = affType.toLowerCase().endsWith('quais'); - if (isQuaiDisplay && quaiName && trainQuai) { - const normDisplayQuai = normalizeQuai(quaiName); - const normTrainQuai = normalizeQuai(trainQuai); - if (normDisplayQuai !== normTrainQuai) { - console.debug(`[useAnnonceSonoreRealtime] Ignoré : voie non correspondante (Afficheur: ${quaiName}, Train: ${trainQuai})`); - return; - } - } - } - - // 1. Couper l'audio en cours de lecture - if (audioRef.current) { - try { audioRef.current.pause(); } catch (e) {} - audioRef.current = null; - } - - // 2. Couper le TTS en cours de lecture - if (typeof window !== 'undefined' && window.speechSynthesis) { - try { window.speechSynthesis.cancel(); } catch (e) {} - } - - // 3. Diffuser l'annonce - if (newRecord.audio_path) { - const { data } = supabase.storage.from('banque-annonces').getPublicUrl(newRecord.audio_path); - const publicUrl = data?.publicUrl; - - if (publicUrl) { - console.debug(`[useAnnonceSonoreRealtime] Lecture audio de : ${publicUrl}`); - const audio = new Audio(publicUrl); - audioRef.current = audio; - audio.play().catch((err) => { - console.error('[useAnnonceSonoreRealtime] Erreur lecture audio:', err); - }); - } - } else if (newRecord.tts_texte) { - if (typeof window !== 'undefined' && window.speechSynthesis) { - console.debug(`[useAnnonceSonoreRealtime] Lecture TTS de : "${newRecord.tts_texte}"`); - const utterance = new SpeechSynthesisUtterance(newRecord.tts_texte); - utterance.lang = 'fr-FR'; - - const voices = window.speechSynthesis.getVoices(); - const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr'))); - const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire')); - - utterance.voice = claireVoice || frenchVoices[0] || null; - window.speechSynthesis.speak(utterance); - } - } - }; - - // --- FALLBACK POLLING (Seule méthode utilisée) --- - // Puisque le WebSocket vers ton instance auto-hébergée (wss://...) est bloqué par le reverse proxy, - // on désactive l'abonnement Realtime pour éviter les erreurs "CHANNEL_ERROR" dans ta console. - // À la place, on interroge la base de données toutes les 2.5 secondes. - const fetchLatest = async () => { - try { - const { data, error } = await supabase - .from('annonces_diffusions') - .select(` - *, - annonce:annonces_enregistrees( - variables_values - ) - `) - .eq('gare_id', gareId) - .order('created_at', { ascending: false }) - .limit(1); - - if (!error && data && data.length > 0) { - const latest = data[0]; - - // Si c'est une annonce qu'on n'a pas encore vue - if (latest.id !== lastAnnonceIdRef.current) { - // S'il s'agit du premier chargement, on mémorise juste l'ID sans la jouer - // sauf si on veut vraiment utiliser diffSeconds, mais pour éviter les problèmes d'horloge - // on se fie au fait que lastAnnonceIdRef.current passe de null à l'ID. - if (lastAnnonceIdRef.current === null) { - lastAnnonceIdRef.current = latest.id; - } else { - // C'est une NOUVELLE annonce apparue pendant qu'on polle - console.debug('[useAnnonceSonoreRealtime] Annonce spontanée détectée :', latest); - lastAnnonceIdRef.current = latest.id; - playAnnonce(latest); - } - } - } - } catch (err) { - // Ignorer les erreurs réseau silencieusement - } - }; - - // On lance le polling toutes les 2.5 secondes pour une réactivité maximale - const pollInterval = setInterval(fetchLatest, 2500); - // On fait aussi un fetch immédiat au démarrage pour initialiser l'ID - fetchLatest(); - - return () => { - console.debug('[useAnnonceSonoreRealtime] Nettoyage...'); - clearInterval(pollInterval); - - if (audioRef.current) { - try { audioRef.current.pause(); } catch (e) {} - audioRef.current = null; - } - if (typeof window !== 'undefined' && window.speechSynthesis) { - try { window.speechSynthesis.cancel(); } catch (e) {} - } - }; - }, [gareId, affType, quaiName]); + // Désactivé : aucune lecture audio ni TTS n'est déclenchée sur les afficheurs. + return null; } + diff --git a/src/hooks/useAutoAnnonce.js b/src/hooks/useAutoAnnonce.js index 989244d..e328ce0 100644 --- a/src/hooks/useAutoAnnonce.js +++ b/src/hooks/useAutoAnnonce.js @@ -1,92 +1,6 @@ -import { useEffect, useRef } from 'react'; - -// Hook pour générer automatiquement les annonces sur l'afficheur (fallback TTS robuste) +// Hook pour annonces automatiques désactivé sur les afficheurs export default function useAutoAnnonce(horaires, currentTime, isArrival = false) { - const announcedRefs = useRef(new Set()); - const isPlayingRef = useRef(false); - - useEffect(() => { - if (!horaires || horaires.length === 0 || !currentTime) return; - - // Déterminer le délai d'annonce (2 min avant l'arrivée ou le départ) - const TRIGGER_MINUTES = 2; - - const checkAndPlay = () => { - if (isPlayingRef.current) return; // Ne pas interrompre si une annonce joue déjà - - for (const train of horaires) { - const numTrain = train.num_train || train.numero_train || train.numero; - if (!numTrain) continue; - - const timeStr = isArrival - ? (train.heure_arrivee || train.arrival_time || train.time || train.heure) - : (train.heure_depart || train.departure_time || train.time || train.heure); - - if (!timeStr) continue; - - // Si timeStr est juste "HH:MM", on crée une date du jour avec cette heure - let trainTime; - if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) { - const [hh, mm] = timeStr.split(':'); - trainTime = new Date(currentTime); - trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0); - } else { - trainTime = new Date(timeStr); - } - - if (isNaN(trainTime.getTime())) continue; - - const diffMs = trainTime.getTime() - currentTime.getTime(); - const diffMins = Math.floor(diffMs / 60000); - - // Si on est dans la minute cible et que l'annonce n'a pas encore été jouée - if (diffMins === TRIGGER_MINUTES && !announcedRefs.current.has(numTrain)) { - announcedRefs.current.add(numTrain); - - const gareObj = isArrival - ? (train.gare_depart_nom || train.gare_depart || train.origine) - : (train.gare_arrivee_nom || train.gare_arrivee || train.destination); - - const gareName = typeof gareObj === 'object' && gareObj !== null ? gareObj.nom : gareObj; - - let voieText = train.voie ? `Voie ${train.voie}.` : ""; - const typeTrain = train.type_train || "train"; - const gareText = gareName || "destination inconnue"; - - const text = isArrival - ? `${voieText} Le ${typeTrain} numéro ${numTrain} en provenance de ${gareText}, va entrer en gare. Éloignez-vous de la bordure du quai.` - : `${voieText} Le ${typeTrain} numéro ${numTrain} à destination de ${gareText}, va partir. Prenez garde à la fermeture des portes.`; - - // Lancement de l'annonce TTS - isPlayingRef.current = true; - - if (typeof window !== 'undefined' && window.speechSynthesis) { - const utterance = new SpeechSynthesisUtterance(text); - utterance.lang = 'fr-FR'; - const voices = window.speechSynthesis.getVoices(); - const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr'))); - const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire')); - utterance.voice = claireVoice || frenchVoices[0] || null; - - utterance.onend = () => { - isPlayingRef.current = false; - }; - utterance.onerror = () => { - isPlayingRef.current = false; - }; - - console.debug(`[useAutoAnnonce] Lecture TTS Automatique: "${text}"`); - window.speechSynthesis.speak(utterance); - } else { - isPlayingRef.current = false; - } - - // On s'arrête après avoir lancé une annonce pour éviter de tout superposer - break; - } - } - }; - - checkAndPlay(); - }, [horaires, currentTime, isArrival]); + // Désactivé : aucune lecture TTS automatique + return null; } + diff --git a/src/pages/Arrivees.js b/src/pages/Arrivees.js index c5957b2..634a2e6 100644 --- a/src/pages/Arrivees.js +++ b/src/pages/Arrivees.js @@ -1,7 +1,21 @@ import React, { useEffect, useState, useMemo } from 'react'; +import WcsButton from '../components/WcsButton'; import sessionMemory from '../lib/sessionMemory'; import { getSourceFromPath, searchGares } from '../services/railwayService'; +const POPULAR_GARES = [ + 'Paris-Gare-de-Lyon', + 'Besançon-Viotte', + 'Dijon-Ville', + 'Lyon-Part-Dieu', + 'Marseille-Saint-Charles', + 'Lille-Flandres', + 'Strasbourg', + 'Bordeaux-Saint-Jean', + 'Nantes', + 'Rennes' +]; + export default function Arrivees({ onBack, navigate }) { const source = getSourceFromPath(window.location.pathname); const prefix = source === 'supabase' ? '/ferrovia' : ''; @@ -10,7 +24,6 @@ export default function Arrivees({ onBack, navigate }) { const [sncfResults, setSncfResults] = useState([]); const [loading, setLoading] = useState(source === 'supabase'); const [error, setError] = useState(null); - const [selected, setSelected] = useState(''); const [search, setSearch] = useState(''); // Pour Supabase : charger les gares une seule fois au montage @@ -51,7 +64,7 @@ export default function Arrivees({ onBack, navigate }) { } catch (e) { if (active) setSncfResults([]); } - }, 200); + }, 180); return () => { active = false; @@ -70,109 +83,146 @@ export default function Arrivees({ onBack, navigate }) { }, [source, allGares, sncfResults, search]); const handleSelectGare = (nom) => { - setSelected(nom); try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} if (navigate) { - navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`); + const cleanNom = String(nom).trim().replace(/\s+/g, '-'); + navigate(`${prefix}/afficheurs/${encodeURIComponent(cleanNom)}?mode=arrivees`); } }; return ( -
-
-

Arrivées

- - {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'} - +
+ {/* Stepper */} +
+
+ 1 + Choix de la gare +
+ › +
+ 2 + Type d'afficheur +
+ › +
+ 3 + Options & Affichage +
- {loading &&

Chargement des gares...

} - {error &&

Erreur : {error}

} - - {!loading && !error && ( -
- {/* Barre de recherche */} -
- setSearch(e.target.value)} - style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} - aria-label="Rechercher une gare" - autoFocus - /> +
+ {/* Header */} +
+
+

🏁 Prochaines Arrivées

+

Recherchez une gare pour afficher le tableau des arrivées en temps réel.

+ + {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'} + +
- {/* Invitation à la recherche en mode SNCF */} - {source === 'sncf' && search.trim().length < 2 && ( -
-

- 🔍 Saisissez au moins 2 caractères pour rechercher parmi les gares du référentiel GTFS SNCF. -

-
- )} - - {/* Aucun résultat */} - {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( -
- Aucune gare SNCF trouvée pour "{search}". -
- )} - - {/* Résultats */} - {displayedGares.length > 0 && ( -
- {displayedGares.map((g) => { - const nom = g.nom; - return ( -
-
handleSelectGare(nom)} - onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} - tabIndex={0} - > -
- {nom} - {g.commune && ({g.commune} - {g.departement})} -
-
- {g.uic && UIC {g.uic}} - {selected === nom &&
Sélectionnée
} -
-
-
-
- ); - })} -
- )} - - {selected && ( -
-

Gare sélectionnée : {selected}

- -
+ {/* Barre de recherche */} +
+ setSearch(e.target.value)} + className="launcher-search-input" + aria-label="Rechercher une gare" + autoFocus + /> + {search && ( + )}
- )} -
- + {/* Suggestions rapides / Gares populaires */} +
+ Gares fréquentes : +
+ {POPULAR_GARES.map((gName) => ( + + ))} +
+
+ + {loading &&

⏳ Chargement des gares...

} + {error &&

⚠️ Erreur : {error}

} + + {/* Invitation à la recherche en mode SNCF */} + {source === 'sncf' && search.trim().length < 2 && ( +
+

+ 💡 Saisissez au moins 2 lettres pour chercher parmi plus de 3 500 gares du réseau français. +

+
+ )} + + {/* Aucun résultat */} + {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( +
+ Aucune gare trouvée pour « {search} ». +
+ )} + + {/* Liste des résultats */} + {displayedGares.length > 0 && ( +
+ {displayedGares.map((g) => { + const nom = g.nom; + return ( +
handleSelectGare(nom)} + onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} + tabIndex={0} + role="listitem" + > +
+
{nom}
+ {g.commune && ( +
+ {g.commune} {g.departement ? `(${g.departement})` : ''} +
+ )} +
+
+ {g.uic && UIC {g.uic}} + › +
+
+ ); + })} +
+ )} + + {/* Navigation retour */} +
+ +
); } + diff --git a/src/pages/Departs.js b/src/pages/Departs.js index 148aa4f..880e1b7 100644 --- a/src/pages/Departs.js +++ b/src/pages/Departs.js @@ -1,7 +1,21 @@ import React, { useEffect, useState, useMemo } from 'react'; +import WcsButton from '../components/WcsButton'; import sessionMemory from '../lib/sessionMemory'; import { getSourceFromPath, searchGares } from '../services/railwayService'; +const POPULAR_GARES = [ + 'Paris-Gare-de-Lyon', + 'Besançon-Viotte', + 'Dijon-Ville', + 'Lyon-Part-Dieu', + 'Marseille-Saint-Charles', + 'Lille-Flandres', + 'Strasbourg', + 'Bordeaux-Saint-Jean', + 'Nantes', + 'Rennes' +]; + export default function Departs({ onBack, navigate }) { const source = getSourceFromPath(window.location.pathname); const prefix = source === 'supabase' ? '/ferrovia' : ''; @@ -10,7 +24,6 @@ export default function Departs({ onBack, navigate }) { const [sncfResults, setSncfResults] = useState([]); const [loading, setLoading] = useState(source === 'supabase'); const [error, setError] = useState(null); - const [selected, setSelected] = useState(''); const [search, setSearch] = useState(''); // Pour Supabase : charger les gares une seule fois au montage @@ -51,7 +64,7 @@ export default function Departs({ onBack, navigate }) { } catch (e) { if (active) setSncfResults([]); } - }, 200); + }, 180); return () => { active = false; @@ -70,109 +83,146 @@ export default function Departs({ onBack, navigate }) { }, [source, allGares, sncfResults, search]); const handleSelectGare = (nom) => { - setSelected(nom); try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} if (navigate) { - navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`); + const cleanNom = String(nom).trim().replace(/\s+/g, '-'); + navigate(`${prefix}/afficheurs/${encodeURIComponent(cleanNom)}?mode=departs`); } }; return ( -
-
-

Départs

- - {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'} - +
+ {/* Stepper */} +
+
+ 1 + Choix de la gare +
+ › +
+ 2 + Type d'afficheur +
+ › +
+ 3 + Options & Affichage +
- {loading &&

Chargement des gares...

} - {error &&

Erreur : {error}

} - - {!loading && !error && ( -
- {/* Barre de recherche */} -
- setSearch(e.target.value)} - style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} - aria-label="Rechercher une gare" - autoFocus - /> +
+ {/* Header */} +
+
+

🚆 Prochains Départs

+

Recherchez une gare pour afficher le tableau des départs en temps réel.

+ + {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'} + +
- {/* Invitation à la recherche en mode SNCF */} - {source === 'sncf' && search.trim().length < 2 && ( -
-

- 🔍 Saisissez au moins 2 caractères pour rechercher parmi les gares du référentiel GTFS SNCF. -

-
- )} - - {/* Aucun résultat */} - {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( -
- Aucune gare SNCF trouvée pour "{search}". -
- )} - - {/* Résultats */} - {displayedGares.length > 0 && ( -
- {displayedGares.map((g) => { - const nom = g.nom; - return ( -
-
handleSelectGare(nom)} - onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} - tabIndex={0} - > -
- {nom} - {g.commune && ({g.commune} - {g.departement})} -
-
- {g.uic && UIC {g.uic}} - {selected === nom &&
Sélectionnée
} -
-
-
-
- ); - })} -
- )} - - {selected && ( -
-

Gare sélectionnée : {selected}

- -
+ {/* Barre de recherche */} +
+ setSearch(e.target.value)} + className="launcher-search-input" + aria-label="Rechercher une gare" + autoFocus + /> + {search && ( + )}
- )} -
- + {/* Suggestions rapides / Gares populaires */} +
+ Gares fréquentes : +
+ {POPULAR_GARES.map((gName) => ( + + ))} +
+
+ + {loading &&

⏳ Chargement des gares...

} + {error &&

⚠️ Erreur : {error}

} + + {/* Invitation à la recherche en mode SNCF */} + {source === 'sncf' && search.trim().length < 2 && ( +
+

+ 💡 Saisissez au moins 2 lettres pour chercher parmi plus de 3 500 gares du réseau français. +

+
+ )} + + {/* Aucun résultat */} + {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && ( +
+ Aucune gare trouvée pour « {search} ». +
+ )} + + {/* Liste des résultats */} + {displayedGares.length > 0 && ( +
+ {displayedGares.map((g) => { + const nom = g.nom; + return ( +
handleSelectGare(nom)} + onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)} + tabIndex={0} + role="listitem" + > +
+
{nom}
+ {g.commune && ( +
+ {g.commune} {g.departement ? `(${g.departement})` : ''} +
+ )} +
+
+ {g.uic && UIC {g.uic}} + › +
+
+ ); + })} +
+ )} + + {/* Navigation retour */} +
+ +
); } + diff --git a/src/pages/Displays.js b/src/pages/Displays.js index d1f0e8c..5833e95 100644 --- a/src/pages/Displays.js +++ b/src/pages/Displays.js @@ -8,10 +8,42 @@ export default function Displays({ onBack, navigate }) { const prefix = source === 'supabase' ? '/ferrovia' : ''; const types = [ - { id: 'classiques', label: 'Classiques' }, - { id: 'eva', label: 'EVA' }, - { id: 'afl', label: 'AFL' }, - { id: 'transilien', label: 'TRANSILIEN' } + { + id: 'afl', + label: 'AFL', + sublabel: 'Afficheurs Fluides & Lumineux', + tag: 'Recommandé / Moderne', + tagType: 'modern', + icon: '🖥️', + desc: 'Écrans modernes haute lisibilité avec cycles Départs / Arrivées, Informations trafic, Sécurité et Correspondances.' + }, + { + id: 'classiques', + label: 'Classiques', + sublabel: 'Téléaffichage Historique', + tag: 'Rétro / Vintage', + tagType: 'retro', + icon: '📺', + desc: 'Reproduction des moniteurs cathodiques et écrans à palettes historiques des gares SNCF.' + }, + { + id: 'eva', + label: 'EVA', + sublabel: 'Écrans Vidéo Voyageurs', + tag: 'Gares Connectées', + tagType: 'modern', + icon: '📱', + desc: 'Écrans d\'information enrichis avec multimédia, infogare et dessertes complètes.' + }, + { + id: 'transilien', + label: 'Transilien', + sublabel: 'Réseau Île-de-France', + tag: 'Régional IDF', + tagType: 'retro', + icon: '🚊', + desc: 'Afficheurs dédiés aux lignes RER et Transilien SNCF (lignes C, D, E, H, J, L, N, P, R, U).' + } ]; // Nettoyage du chemin pour extraire la gare même avec /ferrovia @@ -49,68 +81,98 @@ export default function Displays({ onBack, navigate }) { } if (!mode) mode = 'departs'; + const formattedGareName = gare ? gare.replace(/-/g, ' ') : 'Non sélectionnée'; + return ( -
-
-

Choisissez un type d'afficheur

- - {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'} - +
+ {/* Stepper */} +
+
+ ✓ + {formattedGareName} +
+ › +
+ 2 + Type d'afficheur +
+ › +
+ 3 + Options & Affichage +
- {gare &&

Gare sélectionnée : {gare}

} -

Mode sélectionné : {mode === 'arrivees' ? 'Arrivées' : 'Départs'}

- -
- {types.map((t) => ( -
{ - 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}`); - return; - } - alert(`${t.label} pour ${gare || '—'}`); - }} - > -

{t.label}

-

Afficher le style {t.label} pour la gare {gare || ''}.

+
+ {/* Header */} +
+
+

Sélectionnez le style d'afficheur

+

+ Gare : {formattedGareName} • Mode : {mode === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'} +

- ))} -
+ + {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'} + +
-
- { - try { + {/* Grille de sélection tactile des styles d'afficheurs */} +
+ {types.map((t) => ( +
{ + 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}`); + } + }} + > +
+ {t.icon} + + {t.tag} + +

{t.label}

+
{t.sublabel}
+

{t.desc}

+
+ +
+ + Choisir ce style › + +
+
+ ))} +
+ + {/* Navigation retour */} +
+ { if (typeof onBack === 'function') { onBack(); - return; + } else if (navigate) { + navigate(`${prefix}/${mode}`); } - } catch (e) {} - - if (typeof window !== 'undefined' && window.history && window.history.length > 1) { - window.history.back(); - } else { - window.location.href = prefix || '/'; - } - }} - label="Retour" - /> + }} + label="← Changer de gare" + /> +
); } + diff --git a/src/pages/SelectAfficheur.js b/src/pages/SelectAfficheur.js index a58d6a1..b3abe24 100644 --- a/src/pages/SelectAfficheur.js +++ b/src/pages/SelectAfficheur.js @@ -108,344 +108,268 @@ export default function SelectAfficheur({ onBack, navigate }) { }; return ( -
-
-

- Sélection de l'Afficheur -

- - {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'} - +
+ {/* Stepper */} +
+
+ ✓ + {decodedGare || 'Gare'} +
+ › +
+ ✓ + {typeParam.toUpperCase()} +
+ › +
+ 3 + Options & Affichage +
-

- Pour la gare de {decodedGare} en mode {modeParam === 'arrivees' ? 'Arrivées' : 'Départs'} (Style : {typeParam.toUpperCase()}). -

- -
- {/* General Afficheur Option */} -
-
-

Afficheur Général

-

- Affiche la liste des trains programmés avec possibilité d'inclure les pages d'informations et services. +

+ {/* Header */} +
+
+

Configuration de l'afficheur

+

+ Gare : {decodedGare} • Mode : {modeParam === 'arrivees' ? '🏁 Arrivées' : '🚆 Départs'} • Style : {typeParam.toUpperCase()}

- - {typeParam === 'afl' && ( -
-
- - Pages à inclure dans la rotation : - -
- - | - -
-
- -
- - - - - - - - - -
-
- )}
- + + {source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'} +
- {/* Platform-specific Afficheur Option */} -
-
-

Afficheur par Quai

-

- Affiche le prochain train et sa desserte spécifiquement pour une voie/quai. -

+ {/* Grille responsive 2 colonnes (1 col sur mobile) */} +
+ {/* Option 1 : Afficheur Général */} +
+
+ 🖥️ + Écran Général +

Afficheur Général (Hall)

+

+ Affiche la liste complète des circulations avec basculement automatique des pages. +

- {loading ? ( -
Chargement des voies...
- ) : ( -
- {platforms.length > 0 ? ( -
- - + {typeParam === 'afl' && ( +
+
+ + Pages incluses dans la boucle : + +
+ + | + +
+ +
+ + + + + + + + + +
+
+ )} +
+ + +
+ + {/* Option 2 : Afficheur par Quai */} +
+
+ 🚉 + Écran de Voie +

Afficheur par Quai

+

+ Affiche le prochain train au départ/arrivée avec la liste déroulante des gares desservies sur la voie. +

+ +
+ {loading ? ( +
⏳ Détection des voies réelles...
) : ( -
- Aucune voie détectée automatiquement pour cette gare. +
+ {platforms.length > 0 ? ( +
+ + +
+ ) : ( +
+ ℹ️ Saisie manuelle de la voie ci-dessous. +
+ )} + +
+ + setManualPlatform(e.target.value)} + style={{ + width: '100%', + padding: '12px', + borderRadius: '8px', + border: '2px solid #e2ebf6', + fontSize: '15px', + boxSizing: 'border-box', + background: '#ffffff' + }} + /> +
)} - -
- - setManualPlatform(e.target.value)} - style={{ - width: '100%', - padding: '8px 12px', - borderRadius: '6px', - border: '1px solid #ccc', - fontSize: '14px', - boxSizing: 'border-box' - }} - /> -
- )} +
+ +
- + + + + + +
+
+ )} + + {/* Navigation retour */} +
+ { + if (typeof onBack === 'function') { + onBack(); + } else if (typeof navigate === 'function') { + navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`); + } }} - > - Accéder au Quai - + label="← Retour au choix du style" + />
- - {typeParam === 'afl' && ( -
-

- Accès direct à une page AFL spécifique (fixe) : -

-
- - - - - - -
-
- )} - -
- { - if (typeof onBack === 'function') { - onBack(); - } else if (typeof navigate === 'function') { - navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`); - } - }} - label="Retour aux types d'afficheurs" - /> -
); } + diff --git a/src/styles/Launcher.css b/src/styles/Launcher.css new file mode 100644 index 0000000..192d03c --- /dev/null +++ b/src/styles/Launcher.css @@ -0,0 +1,544 @@ +/* ============================================================================== + Styles Responsive & WCS pour les pages de guidage (Accueil, Recherche, Sélection) + Optimisé pour Mobile, Tablette et Desktop + ============================================================================== */ + +:root { + --sncf-primary: #002257; + --sncf-blue: #00439e; + --sncf-blue-light: #e8f0fe; + --sncf-blue-border: #c2dbfe; + --sncf-accent: #d70022; + --sncf-green: #008b47; + --sncf-green-light: #e6f4ea; + --sncf-bg: #f4f7fb; + --sncf-card-bg: #ffffff; + --sncf-text: #1a1a1a; + --sncf-text-secondary: #5f6368; + --sncf-border: #e2ebf6; + --sncf-radius: 12px; + --sncf-shadow: 0 4px 20px rgba(0, 34, 87, 0.08); + --sncf-shadow-hover: 0 8px 28px rgba(0, 34, 87, 0.14); +} + +/* Conteneur principal responsive */ +.launcher-container { + width: 100%; + max-width: 980px; + margin: 0 auto; + padding: 16px; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +@media (min-width: 768px) { + .launcher-container { + padding: 24px 32px; + } +} + +/* Carte principale */ +.launcher-card { + background: var(--sncf-card-bg); + border-radius: var(--sncf-radius); + box-shadow: var(--sncf-shadow); + padding: 20px; + margin-bottom: 24px; + border: 1px solid var(--sncf-border); +} + +@media (min-width: 768px) { + .launcher-card { + padding: 32px; + } +} + +/* Stepper de navigation (Étapes) */ +.launcher-stepper { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--sncf-border); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.launcher-step { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: var(--sncf-text-secondary); + white-space: nowrap; + flex-shrink: 0; +} + +.launcher-step--active { + color: var(--sncf-blue); +} + +.launcher-step--completed { + color: var(--sncf-green); +} + +.launcher-step-num { + width: 24px; + height: 24px; + border-radius: 50%; + background: #e0e8f6; + color: #555; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; +} + +.launcher-step--active .launcher-step-num { + background: var(--sncf-blue); + color: #ffffff; +} + +.launcher-step--completed .launcher-step-num { + background: var(--sncf-green); + color: #ffffff; +} + +.launcher-step-separator { + color: #ccc; + font-size: 14px; +} + +/* Header & Titres */ +.launcher-header { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; +} + +@media (min-width: 600px) { + .launcher-header { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.launcher-title { + margin: 0; + color: var(--sncf-primary); + font-size: 24px; + font-weight: 700; + letter-spacing: -0.5px; +} + +@media (min-width: 768px) { + .launcher-title { + font-size: 28px; + } +} + +.launcher-badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + padding: 6px 12px; + border-radius: 20px; + background: var(--sncf-blue-light); + color: var(--sncf-blue); + width: fit-content; +} + +.launcher-badge--supabase { + background: var(--sncf-green-light); + color: var(--sncf-green); +} + +.launcher-subtitle { + margin: 0 0 20px 0; + color: var(--sncf-text-secondary); + font-size: 15px; + line-height: 1.5; +} + +/* Grille de sélection tactile */ +.launcher-grid { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + margin-bottom: 24px; +} + +@media (min-width: 600px) { + .launcher-grid--2col { + grid-template-columns: 1fr 1fr; + gap: 20px; + } +} + +@media (min-width: 768px) { + .launcher-grid--4col { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } +} + +@media (min-width: 1024px) { + .launcher-grid--4col { + grid-template-columns: repeat(4, 1fr); + gap: 20px; + } +} + +/* Cartes sélectionnables */ +.launcher-select-card { + border: 2px solid var(--sncf-border); + border-radius: var(--sncf-radius); + padding: 20px; + display: flex; + flex-direction: column; + justifyContent: space-between; + background: #ffffff; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + position: relative; + text-decoration: none; + color: inherit; + box-sizing: border-box; +} + +.launcher-select-card:hover, +.launcher-select-card:focus-visible { + border-color: var(--sncf-blue); + transform: translateY(-2px); + box-shadow: var(--sncf-shadow-hover); + outline: none; +} + +.launcher-select-card--selected { + border-color: var(--sncf-blue); + background: #f8fbff; + box-shadow: 0 0 0 1px var(--sncf-blue); +} + +.launcher-card-icon { + font-size: 32px; + margin-bottom: 12px; + display: inline-block; +} + +.launcher-card-title { + margin: 0 0 8px 0; + color: var(--sncf-blue); + font-size: 19px; + font-weight: 700; +} + +.launcher-card-desc { + margin: 0 0 16px 0; + color: var(--sncf-text-secondary); + font-size: 14px; + line-height: 1.4; + flex-grow: 1; +} + +.launcher-card-tag { + display: inline-block; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 3px 8px; + border-radius: 4px; + background: #eef3fb; + color: #00439e; + margin-bottom: 12px; + width: fit-content; +} + +.launcher-card-tag--retro { + background: #fff0d4; + color: #a35200; +} + +.launcher-card-tag--modern { + background: #e6f4ea; + color: #137333; +} + +/* Barre de recherche tactile */ +.launcher-search-box { + position: relative; + width: 100%; + margin-bottom: 16px; +} + +.launcher-search-input { + width: 100%; + padding: 14px 44px 14px 16px; + font-size: 16px; + border: 2px solid var(--sncf-border); + border-radius: 10px; + background: #ffffff; + color: var(--sncf-text); + box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; + -webkit-appearance: none; +} + +.launcher-search-input:focus { + border-color: var(--sncf-blue); + outline: none; + box-shadow: 0 0 0 3px rgba(0, 67, 158, 0.15); +} + +.launcher-search-clear { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: #e0e0e0; + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 12px; + font-weight: bold; + color: #666; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +/* Raccourcis / Gares populaires (Chips) */ +.launcher-chips-container { + margin-bottom: 20px; +} + +.launcher-chips-label { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--sncf-text-secondary); + margin-bottom: 8px; + display: block; +} + +.launcher-chips-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.launcher-chip { + background: #ffffff; + border: 1px solid var(--sncf-border); + border-radius: 20px; + padding: 8px 14px; + font-size: 13px; + font-weight: 600; + color: var(--sncf-primary); + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + user-select: none; +} + +.launcher-chip:hover, +.launcher-chip:focus-visible { + background: var(--sncf-blue-light); + border-color: var(--sncf-blue-border); + color: var(--sncf-blue); +} + +.launcher-chip--active { + background: var(--sncf-blue); + border-color: var(--sncf-blue); + color: #ffffff; +} + +/* Liste des résultats de recherche */ +.launcher-results-list { + background: #ffffff; + border: 1px solid var(--sncf-border); + border-radius: 10px; + overflow: hidden; + max-height: 400px; + overflow-y: auto; + box-shadow: var(--sncf-shadow); + margin-bottom: 20px; +} + +.launcher-result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + cursor: pointer; + border-bottom: 1px solid #f0f3f8; + transition: background-color 0.15s ease; + min-height: 48px; + box-sizing: border-box; +} + +.launcher-result-item:last-child { + border-bottom: none; +} + +.launcher-result-item:hover, +.launcher-result-item:focus-visible { + background: #f2f7ff; +} + +.launcher-result-name { + font-size: 15px; + font-weight: 600; + color: var(--sncf-primary); +} + +.launcher-result-sub { + font-size: 13px; + color: var(--sncf-text-secondary); + margin-top: 2px; +} + +.launcher-result-tag { + font-size: 11px; + font-weight: 600; + background: #edf2f7; + color: #4a5568; + padding: 3px 8px; + border-radius: 6px; +} + +/* Checklist interactive des pages AFL */ +.launcher-checklist-box { + background: #ffffff; + border: 1px solid var(--sncf-blue-border); + border-radius: 10px; + padding: 16px; + margin-bottom: 20px; +} + +.launcher-checklist-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + flex-wrap: wrap; + gap: 8px; +} + +.launcher-checklist-title { + font-size: 14px; + font-weight: 700; + color: var(--sncf-primary); +} + +.launcher-checklist-actions { + display: flex; + gap: 8px; +} + +.launcher-link-btn { + background: none; + border: none; + color: var(--sncf-blue); + font-size: 12px; + font-weight: 600; + cursor: pointer; + padding: 0; + text-decoration: underline; +} + +.launcher-checkbox-label { + display: flex; + align-items: center; + gap: 12px; + font-size: 14px; + color: var(--sncf-text); + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.15s ease; + user-select: none; + min-height: 40px; + box-sizing: border-box; +} + +.launcher-checkbox-label:hover { + background: #f4f8ff; +} + +.launcher-checkbox { + width: 20px; + height: 20px; + accent-color: var(--sncf-blue); + cursor: pointer; + flex-shrink: 0; +} + +/* Boutons d'action WCS & Standard */ +.launcher-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 14px 20px; + font-size: 15px; + font-weight: 700; + border-radius: 8px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; + text-align: center; + text-decoration: none; + min-height: 48px; +} + +.launcher-btn--primary { + background: var(--sncf-blue); + color: #ffffff; +} + +.launcher-btn--primary:hover { + background: #00337c; +} + +.launcher-btn--success { + background: var(--sncf-green); + color: #ffffff; +} + +.launcher-btn--success:hover { + background: #006e38; +} + +.launcher-btn--secondary { + background: #eef3fb; + color: var(--sncf-blue); + border: 1px solid var(--sncf-blue-border); +} + +.launcher-btn--secondary:hover { + background: var(--sncf-blue-light); +} + +/* Pied de page de navigation (Retour) */ +.launcher-footer-nav { + border-top: 1px solid var(--sncf-border); + padding-top: 20px; + display: flex; + justify-content: flex-start; +}