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

-> IMPLÉMENTATION DONNÉES GTFS / GTFS-RT / SIRI LTE SNCF
-> RESTRUCTURATION DE L'APP.

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES
-> AMÉLIORATION DES
This commit is contained in:
mathis committed 2026-08-30 01:58:16 +02:00
1 parent 0e0d8c435f
commit 72de543dc6
18 files changed
+1550 -544

No files matched your search

+124 -61
View File
@@ -1,103 +1,168 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import React, { useEffect, useState, useMemo } from 'react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Departs({ onBack, navigate }) {
const [gares, setGares] = useState([]);
const [loading, setLoading] = useState(true);
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const [allGares, setAllGares] = useState([]);
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
useEffect(() => {
let mounted = true;
async function loadGares() {
setLoading(true);
setError(null);
if (!supabase) {
// Le client Supabase n'a pas pu être initialisé (variables d'env manquantes)
if (mounted) {
setError(
"Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)."
);
setLoading(false);
if (source === 'supabase') {
async function loadSupabaseGares() {
setLoading(true);
setError(null);
try {
const data = await searchGares('', 'supabase');
if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
}
loadSupabaseGares();
}
return () => { mounted = false; };
}, [source]);
// Pour SNCF : recherche dynamique avec debounce dès 2 caractères
useEffect(() => {
let active = true;
if (source === 'sncf') {
const q = search.trim();
if (q.length < 2) {
setSncfResults([]);
return;
}
try {
const { data, error } = await supabase.from('gares').select('nom');
if (error) throw error;
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
} finally {
if (mounted) setLoading(false);
}
const timer = setTimeout(async () => {
try {
const results = await searchGares(q, 'sncf');
if (active) setSncfResults(results);
} catch (e) {
if (active) setSncfResults([]);
}
}, 200);
return () => {
active = false;
clearTimeout(timer);
};
}
}, [search, source]);
loadGares();
// Liste finale affichée
const displayedGares = useMemo(() => {
if (source === 'supabase') {
const q = search.toLowerCase().trim();
return allGares.filter(nom => String(nom).toLowerCase().includes(q)).map(nom => ({ nom }));
}
return sncfResults;
}, [source, allGares, sncfResults, search]);
return () => {
mounted = false;
};
}, []);
const handleSelectGare = (nom) => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}
};
return (
<div style={{ padding: 20 }}>
<h1>Départs</h1>
<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>
{loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && (
<div>
{/* Barre de recherche centrée */}
{/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input
type="search"
placeholder="Rechercher une gare..."
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>
{/* Résultats sous forme de lignes */}
<div role="list" aria-label="Résultats des gares">
{gares
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase()))
.map((nom) => (
<div key={nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }}
onClick={() => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}}
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`) : setSelected(nom)) : null)}
tabIndex={0}
>
<div>{nom}</div>
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</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 style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} />
</div>
))}
</div>
);
})}
</div>
)}
{selected && (
<div style={{ marginTop: 12 }}>
<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(`/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
} else {
alert(`Afficher les départs pour ${selected}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
}
}}>Voir les départs</button>
</div>
@@ -111,5 +176,3 @@ export default function Departs({ onBack, navigate }) {
</div>
);
}