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:
1 parent
0e0d8c435f
commit
72de543dc6
18 files changed
+1550
-544
No files matched your search
+124
-61
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user