Désactivation des annonces sonores et refonte responsive mobile/tablette des pages de guidage WCS
This commit is contained in:
1 parent
4f3a34b101
commit
983f4f0160
9 files changed
+1315
-842
No files matched your search
@@ -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");
|
||||
|
||||
+114
-46
@@ -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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '32px 24px', alignItems: 'center', background: '#e9f0ff', borderRadius: '12px' }}>
|
||||
<div style={{ width: '100%', textAlign: 'center', marginBottom: 20 }}>
|
||||
<div style={{ display: 'inline-block', padding: '4px 12px', borderRadius: '20px', background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe', color: source === 'supabase' ? '#137333' : '#1a73e8', fontWeight: 600, fontSize: '13px', marginBottom: '8px' }}>
|
||||
{source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'}
|
||||
</div>
|
||||
<h1 style={{ margin: '8px 0', fontSize: 28, color: '#002257' }}>Accueil</h1>
|
||||
<p style={{ margin: '4px 0 16px', color: '#555' }}>
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%', maxWidth: '800px' }}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => { 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"
|
||||
>
|
||||
<h2 style={{ color: '#00439e', marginTop: 0 }}>🚆 Départs</h2>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.</p>
|
||||
<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>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ Mode Supabase' : '🌐 Mode SNCF Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => { 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"
|
||||
>
|
||||
<h2 style={{ color: '#00439e', marginTop: 0 }}>🏁 Arrivées</h2>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochaines arrivées d'une gare en temps réel.</p>
|
||||
{/* 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 === ' ') {
|
||||
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>
|
||||
|
||||
<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 === ' ') {
|
||||
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>
|
||||
</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 }}>
|
||||
✨ <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>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+141
-91
@@ -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 (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<h1 style={{ margin: 0 }}>Arrivées</h1>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
|
||||
color: source === 'supabase' ? '#137333' : '#1a73e8'
|
||||
}}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
{loading && <p>Chargement des gares...</p>}
|
||||
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div>
|
||||
{/* Barre de recherche */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
|
||||
<input
|
||||
type="search"
|
||||
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
|
||||
aria-label="Rechercher une gare"
|
||||
autoFocus
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Invitation à la recherche en mode SNCF */}
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
|
||||
<p style={{ margin: 0, fontSize: '15px' }}>
|
||||
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aucun résultat */}
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
|
||||
Aucune gare SNCF trouvée pour "{search}".
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Résultats */}
|
||||
{displayedGares.length > 0 && (
|
||||
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<div key={g.uic || nom} role="listitem">
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }}
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ fontSize: '15px' }}>{nom}</strong>
|
||||
{g.commune && <span style={{ marginLeft: '8px', color: '#666', fontSize: '13px' }}>({g.commune} - {g.departement})</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <span style={{ fontSize: '11px', color: '#888', background: '#f1f3f4', padding: '2px 6px', borderRadius: '4px' }}>UIC {g.uic}</span>}
|
||||
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p>Gare sélectionnée : <strong>{selected}</strong></p>
|
||||
<button onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
|
||||
if (navigate) {
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
|
||||
}
|
||||
}}>Voir les arrivées</button>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={onBack}>Retour</button>
|
||||
{/* 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>
|
||||
</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>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={onBack}
|
||||
label="← Retour à l'accueil"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+141
-91
@@ -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 (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<h1 style={{ margin: 0 }}>Départs</h1>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
|
||||
color: source === 'supabase' ? '#137333' : '#1a73e8'
|
||||
}}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
{loading && <p>Chargement des gares...</p>}
|
||||
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div>
|
||||
{/* Barre de recherche */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
|
||||
<input
|
||||
type="search"
|
||||
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
|
||||
aria-label="Rechercher une gare"
|
||||
autoFocus
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS Live'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Invitation à la recherche en mode SNCF */}
|
||||
{source === 'sncf' && search.trim().length < 2 && (
|
||||
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
|
||||
<p style={{ margin: 0, fontSize: '15px' }}>
|
||||
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aucun résultat */}
|
||||
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
|
||||
Aucune gare SNCF trouvée pour "{search}".
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Résultats */}
|
||||
{displayedGares.length > 0 && (
|
||||
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
|
||||
{displayedGares.map((g) => {
|
||||
const nom = g.nom;
|
||||
return (
|
||||
<div key={g.uic || nom} role="listitem">
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }}
|
||||
onClick={() => handleSelectGare(nom)}
|
||||
onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ fontSize: '15px' }}>{nom}</strong>
|
||||
{g.commune && <span style={{ marginLeft: '8px', color: '#666', fontSize: '13px' }}>({g.commune} - {g.departement})</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{g.uic && <span style={{ fontSize: '11px', color: '#888', background: '#f1f3f4', padding: '2px 6px', borderRadius: '4px' }}>UIC {g.uic}</span>}
|
||||
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p>Gare sélectionnée : <strong>{selected}</strong></p>
|
||||
<button onClick={() => {
|
||||
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
|
||||
if (navigate) {
|
||||
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
|
||||
}
|
||||
}}>Voir les départs</button>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={onBack}>Retour</button>
|
||||
{/* 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>
|
||||
</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>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={onBack}
|
||||
label="← Retour à l'accueil"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+119
-57
@@ -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 (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<h1 style={{ margin: 0 }}>Choisissez un type d'afficheur</h1>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
|
||||
color: source === 'supabase' ? '#137333' : '#1a73e8'
|
||||
}}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
{gare && <p>Gare sélectionnée : <strong>{gare}</strong></p>}
|
||||
<p>Mode sélectionné : <strong>{mode === 'arrivees' ? 'Arrivées' : 'Départs'}</strong></p>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16 }}>
|
||||
{types.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
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 || '—'}`);
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: '0 0 8px' }}>{t.label}</h3>
|
||||
<p style={{ margin: 0, color: '#555' }}>Afficher le style {t.label} pour la gare {gare || ''}.</p>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
className="wcs-primary"
|
||||
onClick={() => {
|
||||
try {
|
||||
{/* 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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation retour */}
|
||||
<div className="launcher-footer-nav">
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={() => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+245
-321
@@ -108,344 +108,268 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '780px',
|
||||
margin: '40px auto',
|
||||
padding: '32px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<h2 style={{
|
||||
margin: 0,
|
||||
color: '#002257',
|
||||
fontSize: '28px',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
Sélection de l'Afficheur
|
||||
</h2>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
|
||||
color: source === 'supabase' ? '#137333' : '#1a73e8'
|
||||
}}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<p style={{
|
||||
margin: '0 0 24px 0',
|
||||
color: '#555555',
|
||||
fontSize: '16px',
|
||||
lineHeight: 1.5
|
||||
}}>
|
||||
Pour la gare de <strong>{decodedGare}</strong> en mode <strong>{modeParam === 'arrivees' ? 'Arrivées' : 'Départs'}</strong> (Style : <strong>{typeParam.toUpperCase()}</strong>).
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', marginBottom: '32px' }}>
|
||||
{/* General Afficheur Option */}
|
||||
<div style={{
|
||||
border: '2px solid #e2ebf6',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
background: '#fafbfc'
|
||||
}}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur Général</h3>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
||||
Affiche la liste des trains programmés avec possibilité d'inclure les pages d'informations et services.
|
||||
<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>
|
||||
|
||||
{typeParam === 'afl' && (
|
||||
<div style={{
|
||||
background: '#ffffff',
|
||||
border: '1px solid #d0e1fd',
|
||||
borderRadius: '8px',
|
||||
padding: '14px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<span style={{ fontSize: '13px', fontWeight: 700, color: '#002257' }}>
|
||||
Pages à inclure dans la rotation :
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectAllAflPages(true)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#00439e',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
>
|
||||
Tout cocher
|
||||
</button>
|
||||
<span style={{ color: '#ccc', fontSize: '11px' }}>|</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectAllAflPages(false)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
>
|
||||
Horaires seuls
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.horaires}
|
||||
onChange={() => toggleAflPage('horaires')}
|
||||
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||
/>
|
||||
<span><strong>Page 1 & 2</strong> (Horaires des trains)</span>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.infos}
|
||||
onChange={() => toggleAflPage('infos')}
|
||||
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||
/>
|
||||
<span><strong>Informations</strong> (Trafic & Travaux)</span>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.securite1}
|
||||
onChange={() => toggleAflPage('securite1')}
|
||||
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||
/>
|
||||
<span><strong>Sécurité 1</strong> (Bagages / Objets délaissés)</span>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.securite2}
|
||||
onChange={() => toggleAflPage('securite2')}
|
||||
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||
/>
|
||||
<span><strong>Sécurité 2</strong> (Vigipirate / Alerte 3117)</span>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#222', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aflPages.corresp}
|
||||
onChange={() => toggleAflPage('corresp')}
|
||||
style={{ width: '16px', height: '16px', accentColor: '#00439e' }}
|
||||
/>
|
||||
<span><strong>Correspondances</strong> (Transports urbains & bus)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGeneral}
|
||||
style={{
|
||||
backgroundColor: '#00439e',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 20px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
>
|
||||
Accéder à l'Afficheur
|
||||
</button>
|
||||
<span className={`launcher-badge ${source === 'supabase' ? 'launcher-badge--supabase' : ''}`}>
|
||||
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF GTFS'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Platform-specific Afficheur Option */}
|
||||
<div style={{
|
||||
border: '2px solid #e2ebf6',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
background: '#fafbfc'
|
||||
}}>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 12px 0', color: '#00439e', fontSize: '20px' }}>Afficheur par Quai</h3>
|
||||
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px', lineHeight: 1.4 }}>
|
||||
Affiche le prochain train et sa desserte spécifiquement pour une voie/quai.
|
||||
</p>
|
||||
{/* 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' }}>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ fontSize: '14px', color: '#888', marginBottom: '16px' }}>Chargement des voies...</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{platforms.length > 0 ? (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 600, color: '#333', marginBottom: '4px' }}>
|
||||
Sélectionner une voie existante :
|
||||
</label>
|
||||
<select
|
||||
value={selectedPlatform}
|
||||
onChange={(e) => {
|
||||
setSelectedPlatform(e.target.value);
|
||||
setManualPlatform('');
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ccc',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{platforms.map(p => (
|
||||
<option key={p} value={p}>Voie {p}</option>
|
||||
))}
|
||||
</select>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGeneral}
|
||||
className="launcher-btn launcher-btn--primary"
|
||||
>
|
||||
🖥️ Lancer l'Afficheur Général
|
||||
</button>
|
||||
</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={{ 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 style={{ fontSize: '13px', color: '#ff6b6b', marginBottom: '12px' }}>
|
||||
Aucune voie détectée automatiquement pour cette gare.
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '13px', fontWeight: 600, color: '#333', marginBottom: '4px' }}>
|
||||
Saisie libre d'une voie :
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ex: A, 2 bis, 4..."
|
||||
value={manualPlatform}
|
||||
onChange={(e) => setManualPlatform(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #ccc',
|
||||
fontSize: '14px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePlatform}
|
||||
className="launcher-btn launcher-btn--success"
|
||||
>
|
||||
🚉 Lancer l'Afficheur de Quai
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePlatform}
|
||||
style={{
|
||||
backgroundColor: '#008b47',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 20px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
transition: 'background-color 0.2s'
|
||||
</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={() => {
|
||||
if (typeof onBack === 'function') {
|
||||
onBack();
|
||||
} else if (typeof navigate === 'function') {
|
||||
navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Accéder au Quai
|
||||
</button>
|
||||
label="← Retour au choix du style"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{typeParam === 'afl' && (
|
||||
<div style={{
|
||||
borderTop: '1px solid #eef2f8',
|
||||
paddingTop: '20px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px', color: '#555', fontWeight: 600 }}>
|
||||
Accès direct à une page AFL spécifique (fixe) :
|
||||
</h4>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={() => handleSinglePage('1')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Horaires (Page 1)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSinglePage('2')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Horaires (Page 2)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSinglePage('infos')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Informations
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSinglePage('securite1')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Sécurité 1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSinglePage('securite2')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Sécurité 2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSinglePage('corresp')}
|
||||
style={{ padding: '6px 12px', fontSize: '12px', borderRadius: '4px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
Correspondances
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
borderTop: '1px solid #eeeeee',
|
||||
paddingTop: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start'
|
||||
}}>
|
||||
<WcsButton
|
||||
mode="plain"
|
||||
onClick={() => {
|
||||
if (typeof onBack === 'function') {
|
||||
onBack();
|
||||
} else if (typeof navigate === 'function') {
|
||||
navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
|
||||
}
|
||||
}}
|
||||
label="Retour aux types d'afficheurs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user