Patch Build App

This commit is contained in:
Ferrovia committed 2026-04-10 21:54:37 +02:00
1 parent 3fcdf7e5ef
commit b5df269cf8
5 files changed
+40 -30

No files matched your search

+27
View File
@@ -20,6 +20,33 @@
</PendingDiffInfo>
</value>
</entry>
<entry key="$PROJECT_DIR$/src/pages/afficheurs/classiques/ClassiquesDeparts.js">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/src/pages/afficheurs/classiques/ClassiquesDeparts.js" />
<option name="originalContent" value="import React, { useEffect, useState, useCallback } from 'react';&#10;import './ClassiquesDeparts.css';&#10;import supabase from '../../../lib/supabase';&#10;&#10;// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)&#10;const formatTimeNoSeconds = (v) =&gt; {&#10; if (v == null) return '';&#10; const tryDate = new Date(v);&#10; if (!isNaN(tryDate.getTime())) {&#10;&#9;const hh = String(tryDate.getHours()).padStart(2, '0');&#10;&#9;const mm = String(tryDate.getMinutes()).padStart(2, '0');&#10;&#9;return `${hh}h${mm}`;&#10; }&#10; if (typeof v === 'string') {&#10;&#9;const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);&#10;&#9;if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;&#10;&#9;const m2 = v.match(/T(\d{2}):(\d{2})/);&#10;&#9;if (m2) return `${m2[1]}h${m2[2]}`;&#10;&#9;return v.slice(0,5).replace(':','h');&#10; }&#10; return String(v).slice(0,5).replace(':','h');&#10;};&#10;&#10;// Données proviennent exclusivement de la table `horaires`.&#10;function Logo({op}){&#10; // Tente de charger un logo SVG depuis /type/ en fonction de l'opérateur.&#10; // mapping pour opérateurs connus -&gt; noms de fichiers dans public/type&#10; const mapping = {&#10;&#9;'TGV inOUI': 'logo-inoui.svg',&#10;&#9;'TGV inOUI ': 'logo-inoui.svg',&#10;&#9;'TGV': 'logo-tgv.svg',&#10;&#9;'TER': 'sncf-voyageurs-logo.png',&#10;&#9;'MOBIGO': 'logo-mobigo.svg',&#10;&#9;'TGV Lyria': 'logo-lyria.svg',&#10;&#9;'TGV Lyria ': 'logo-lyria.svg',&#10; };&#10;&#10; const defaultFileFromOp = (name) =&gt; {&#10;&#9;if (!name) return null;&#10;&#9;// nettoyer et fabriquer un nom plausible : &quot;logo-&lt;slug&gt;.svg&quot;&#10;&#9;const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');&#10;&#9;return `logo-${slug}.svg`;&#10; };&#10;&#10; const fileName = mapping[op] || mapping[op.split(' ')[0]] || defaultFileFromOp(op);&#10;&#10; const [hasImg, setHasImg] = React.useState(true);&#10;&#10; return (&#10;&#9;&lt;div className=&quot;aff-classiques-departs__logo-box&quot; aria-hidden&gt;&#10;&#9; {hasImg &amp;&amp; fileName ? (&#10;&#9;&#9;&lt;img&#10;&#9;&#9; src={`/type/${fileName}`}&#10;&#9;&#9; alt={op}&#10;&#9;&#9; className=&quot;aff-classiques-departs__op-logo&quot;&#10;&#9;&#9; onError={()=&gt; setHasImg(false)}&#10;&#9;&#9;/&gt;&#10;&#9; ) : (&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__logo-text&quot; aria-hidden&gt;{op.split(' ')[0]}&lt;/div&gt;&#10;&#9; )}&#10;&#9;&lt;/div&gt;&#10; );&#10;}&#10;&#10;// (La fonction TrainType a été retirée car la colonne affiche désormais directement les&#10;// informations via .train-meta dans la colonne .col-train-type)&#10;&#10;function Clock(){&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10;&#9;const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10;&#9;return () =&gt; clearInterval(id);&#10; }, []);&#10;&#10; const hh = String(now.getHours()).padStart(2, '0');&#10; const mm = String(now.getMinutes()).padStart(2, '0');&#10; const ss = String(now.getSeconds()).padStart(2, '0');&#10;&#10; return (&#10;&#9;&lt;div className=&quot;aff-classiques-departs__clock-container&quot; aria-hidden&gt;&#10;&#9; &lt;div className=&quot;aff-classiques-departs__clock-box&quot;&gt;&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__clock-main&quot;&gt;{hh}:{mm}&lt;/div&gt;&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__clock-seconds&quot;&gt;{ss}&lt;/div&gt;&#10;&#9; &lt;/div&gt;&#10;&#9;&lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesDeparts(){&#10; // lire le paramètre de requête ?gare=Nom-de-la-gare&#10; const params = new URLSearchParams(window.location.search);&#10; const gareParam = params.get('gare');&#10; // support pour ?gare_id=123 ou ?gareId=123&#10; const gareIdParam = params.get('gare_id') || params.get('gareId');&#10; // gérer les +, décoder et remplacer les tirets par des espaces&#10; const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;&#10;&#10; // debug flag activable via ?debug=1 or ?debug=true&#10; const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');&#10; const debug = useCallback((...args) =&gt; { if (debugEnabled) console.debug(...args); }, [debugEnabled]);&#10;&#10; // État pour alterner l'affichage toutes les 3 secondes&#10; const [showStatus, setShowStatus] = useState(false);&#10;&#10; // Données depuis Supabase&#10; const [horaires, setHoraires] = useState([]);&#10; const [allHoraires, setAllHoraires] = useState([]);&#10; const [perturbations, setPerturbations] = useState([]);&#10; // type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)&#10; const [stationType, setStationType] = useState(null);&#10; const [, setTravaux] = useState([]);&#10; const [, setLoading] = useState(false);&#10; const [, setError] = uLine truncated
<option name="updatedContent" value="import React, { useEffect, useState, useCallback } from 'react';&#10;import './ClassiquesDeparts.css';&#10;import supabase from '../../../lib/supabase';&#10;&#10;// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)&#10;const formatTimeNoSeconds = (v) =&gt; {&#10; if (v == null) return '';&#10; const tryDate = new Date(v);&#10; if (!isNaN(tryDate.getTime())) {&#10;&#9;const hh = String(tryDate.getHours()).padStart(2, '0');&#10;&#9;const mm = String(tryDate.getMinutes()).padStart(2, '0');&#10;&#9;return `${hh}h${mm}`;&#10; }&#10; if (typeof v === 'string') {&#10;&#9;const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);&#10;&#9;if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`;&#10;&#9;const m2 = v.match(/T(\d{2}):(\d{2})/);&#10;&#9;if (m2) return `${m2[1]}h${m2[2]}`;&#10;&#9;return v.slice(0,5).replace(':','h');&#10; }&#10; return String(v).slice(0,5).replace(':','h');&#10;};&#10;&#10;// Données proviennent exclusivement de la table `horaires`.&#10;function Logo({op}){&#10; // Tente de charger un logo SVG depuis /type/ en fonction de l'opérateur.&#10; // mapping pour opérateurs connus -&gt; noms de fichiers dans public/type&#10; const mapping = {&#10;&#9;'TGV inOUI': 'logo-inoui.svg',&#10;&#9;'TGV inOUI ': 'logo-inoui.svg',&#10;&#9;'TGV': 'logo-tgv.svg',&#10;&#9;'TER': 'sncf-voyageurs-logo.png',&#10;&#9;'MOBIGO': 'logo-mobigo.svg',&#10;&#9;'TGV Lyria': 'logo-lyria.svg',&#10;&#9;'TGV Lyria ': 'logo-lyria.svg',&#10; };&#10;&#10; const defaultFileFromOp = (name) =&gt; {&#10;&#9;if (!name) return null;&#10;&#9;// nettoyer et fabriquer un nom plausible : &quot;logo-&lt;slug&gt;.svg&quot;&#10;&#9;const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');&#10;&#9;return `logo-${slug}.svg`;&#10; };&#10;&#10; const fileName = mapping[op] || mapping[op.split(' ')[0]] || defaultFileFromOp(op);&#10;&#10; const [hasImg, setHasImg] = React.useState(true);&#10;&#10; return (&#10;&#9;&lt;div className=&quot;aff-classiques-departs__logo-box&quot; aria-hidden&gt;&#10;&#9; {hasImg &amp;&amp; fileName ? (&#10;&#9;&#9;&lt;img&#10;&#9;&#9; src={`/type/${fileName}`}&#10;&#9;&#9; alt={op}&#10;&#9;&#9; className=&quot;aff-classiques-departs__op-logo&quot;&#10;&#9;&#9; onError={()=&gt; setHasImg(false)}&#10;&#9;&#9;/&gt;&#10;&#9; ) : (&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__logo-text&quot; aria-hidden&gt;{op.split(' ')[0]}&lt;/div&gt;&#10;&#9; )}&#10;&#9;&lt;/div&gt;&#10; );&#10;}&#10;&#10;// (La fonction TrainType a été retirée car la colonne affiche désormais directement les&#10;// informations via .train-meta dans la colonne .col-train-type)&#10;&#10;function Clock(){&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10;&#9;const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10;&#9;return () =&gt; clearInterval(id);&#10; }, []);&#10;&#10; const hh = String(now.getHours()).padStart(2, '0');&#10; const mm = String(now.getMinutes()).padStart(2, '0');&#10; const ss = String(now.getSeconds()).padStart(2, '0');&#10;&#10; return (&#10;&#9;&lt;div className=&quot;aff-classiques-departs__clock-container&quot; aria-hidden&gt;&#10;&#9; &lt;div className=&quot;aff-classiques-departs__clock-box&quot;&gt;&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__clock-main&quot;&gt;{hh}:{mm}&lt;/div&gt;&#10;&#9;&#9;&lt;div className=&quot;aff-classiques-departs__clock-seconds&quot;&gt;{ss}&lt;/div&gt;&#10;&#9; &lt;/div&gt;&#10;&#9;&lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesDeparts(){&#10; // lire le paramètre de requête ?gare=Nom-de-la-gare&#10; const params = new URLSearchParams(window.location.search);&#10; const gareParam = params.get('gare');&#10; // support pour ?gare_id=123 ou ?gareId=123&#10; const gareIdParam = params.get('gare_id') || params.get('gareId');&#10; // gérer les +, décoder et remplacer les tirets par des espaces&#10; const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;&#10;&#10; // debug flag activable via ?debug=1 or ?debug=true&#10; const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');&#10; const debug = useCallback((...args) =&gt; { if (debugEnabled) console.debug(...args); }, [debugEnabled]);&#10;&#10; // État pour alterner l'affichage toutes les 3 secondes&#10; const [showStatus, setShowStatus] = useState(false);&#10;&#10; // Données depuis Supabase&#10; const [horaires, setHoraires] = useState([]);&#10; const [allHoraires, setAllHoraires] = useState([]);&#10; const [perturbations, setPerturbations] = useState([]);&#10; // type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)&#10; const [stationType, setStationType] = useState(null);&#10; const [, setTravaux] = useState([]);&#10; const [, setLoading] = useState(false);&#10; const [, setError] = usLine truncated
</PendingDiffInfo>
</value>
</entry>
<entry key="$PROJECT_DIR$/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js" />
<option name="originalContent" value="import React, { useEffect, useState, useRef, useCallback } from 'react';&#10;import './ClassiquesArriveesQuais.css';&#10;import '../ClassiquesArrivees.css';&#10;&#10;/*&#10; ClassiquesArriveesQuais.js&#10; Variante &quot;Arrivées - quais&quot; du composant quais.&#10;&#10; Différences principales avec la version départs:&#10; - utilise `heure_arrivee` / `gare_arrivee_*` lorsque disponible pour afficher l'heure et la destination&#10; - watermark affiché: &quot;arrivée&quot;&#10; - styles importés depuis `ClassiquesArriveesQuais.css` qui reprend la palette de `ClassiquesArrivees.css`&#10;&#10; Champs utilisés pour le calcul/affichage:&#10; - horaire.heure_arrivee, horaire.time, horaire.heure&#10; - horaire.gare_arrivee_nom, horaire.gare_arrivee&#10; - stop.heure_arrivee, stop.gare_nom, stop.gare_id&#10; - perturbations: mêmes clés que pour les départs (retard, retard_minutes, perturbations_arrets, etc.)&#10;*/&#10;import supabase from '../../../../lib/supabase';&#10;&#10;// Hook pour récupérer la map des logos depuis /type/data.json&#10;function useTypeLogos(){&#10; const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });&#10; useEffect(() =&gt; {&#10; let mounted = true;&#10;&#10; const load = async () =&gt; {&#10; const candidates = ['/type/quais/data-color.json', '/type/data.json'];&#10; for(const url of candidates){&#10; try{&#10; const res = await fetch(url);&#10; if(!res.ok) continue;&#10; const data = await res.json();&#10; if(!mounted) return;&#10; const logos = {};&#10; if(Array.isArray(data.logos)){&#10; data.logos.forEach(item =&gt; {&#10; // item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path&#10; let file = item.file;&#10; if(!file &amp;&amp; item.path){&#10; const parts = String(item.path).split('/');&#10; file = parts[parts.length-1];&#10; }&#10; if(!file) return;&#10; const basename = file.replace(/\.[^/.]+$/, '');&#10; const semantic = basename.replace(/^logo-/, '');&#10; const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; // Prefer color logos in the 'quais' subfolder if data came from the quais file&#10; if(url.includes('/quais/')){&#10; logos[key] = `/type/quais/${file}`;&#10; } else {&#10; logos[key] = `/type/${file}`;&#10; }&#10; });&#10; }&#10; logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';&#10; setMap(logos);&#10; return;&#10; }catch(e){&#10; // try next candidate (ignore error and continue loop)&#10; }&#10; }&#10; // no candidate succeeded — keep fallback&#10; };&#10;&#10; load();&#10; return () =&gt; { mounted = false; };&#10; }, []);&#10; return map;&#10;}&#10;&#10;function getTypeLogo(type, TYPE_LOGOS_MAP){&#10; const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };&#10; if(!type) return map.default;&#10; const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; if(map[norm]) return map[norm];&#10; const keys = Object.keys(map).filter(k =&gt; k !== 'default');&#10; for(const k of keys){&#10; if(k.includes(norm) || norm.includes(k)) return map[k];&#10; }&#10; return map.default;&#10;}&#10;&#10;function Clock() {&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10; const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10; return () =&gt; clearInterval(id);&#10; }, []);&#10; const hh = String(now.getHours()).padStart(2,'0');&#10; const mm = String(now.getMinutes()).padStart(2,'0');&#10; const ss = String(now.getSeconds()).padStart(2,'0');&#10; return (&#10; &lt;div className=&quot;aff-quais__clock&quot;&gt;&#10; &lt;div className=&quot;aff-quais__clock-box&quot;&gt;&lt;span className=&quot;aff-quais__clock-main&quot;&gt;{hh}:{mm}:{ss}&lt;/span&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesArriveesQuais(){&#10; const typeLogos = useTypeLogos();&#10;&#10; const rightRef = useRef(null);&#10; const stopsListRef = useRef(null);&#1Line truncated
<option name="updatedContent" value="import React, { useEffect, useState, useRef, useCallback } from 'react';&#10;import './ClassiquesArriveesQuais.css';&#10;import '../ClassiquesArrivees.css';&#10;&#10;/*&#10; ClassiquesArriveesQuais.js&#10; Variante &quot;Arrivées - quais&quot; du composant quais.&#10;&#10; Différences principales avec la version départs:&#10; - utilise `heure_arrivee` / `gare_arrivee_*` lorsque disponible pour afficher l'heure et la destination&#10; - watermark affiché: &quot;arrivée&quot;&#10; - styles importés depuis `ClassiquesArriveesQuais.css` qui reprend la palette de `ClassiquesArrivees.css`&#10;&#10; Champs utilisés pour le calcul/affichage:&#10; - horaire.heure_arrivee, horaire.time, horaire.heure&#10; - horaire.gare_arrivee_nom, horaire.gare_arrivee&#10; - stop.heure_arrivee, stop.gare_nom, stop.gare_id&#10; - perturbations: mêmes clés que pour les départs (retard, retard_minutes, perturbations_arrets, etc.)&#10;*/&#10;import supabase from '../../../../lib/supabase';&#10;&#10;// Hook pour récupérer la map des logos depuis /type/data.json&#10;function useTypeLogos(){&#10; const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });&#10; useEffect(() =&gt; {&#10; let mounted = true;&#10;&#10; const load = async () =&gt; {&#10; const candidates = ['/type/quais/data-color.json', '/type/data.json'];&#10; for(const url of candidates){&#10; try{&#10; const res = await fetch(url);&#10; if(!res.ok) continue;&#10; const data = await res.json();&#10; if(!mounted) return;&#10; const logos = {};&#10; if(Array.isArray(data.logos)){&#10; data.logos.forEach(item =&gt; {&#10; // item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path&#10; let file = item.file;&#10; if(!file &amp;&amp; item.path){&#10; const parts = String(item.path).split('/');&#10; file = parts[parts.length-1];&#10; }&#10; if(!file) return;&#10; const basename = file.replace(/\.[^/.]+$/, '');&#10; const semantic = basename.replace(/^logo-/, '');&#10; const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; // Prefer color logos in the 'quais' subfolder if data came from the quais file&#10; if(url.includes('/quais/')){&#10; logos[key] = `/type/quais/${file}`;&#10; } else {&#10; logos[key] = `/type/${file}`;&#10; }&#10; });&#10; }&#10; logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';&#10; setMap(logos);&#10; return;&#10; }catch(e){&#10; // try next candidate (ignore error and continue loop)&#10; }&#10; }&#10; // no candidate succeeded — keep fallback&#10; };&#10;&#10; load();&#10; return () =&gt; { mounted = false; };&#10; }, []);&#10; return map;&#10;}&#10;&#10;function getTypeLogo(type, TYPE_LOGOS_MAP){&#10; const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };&#10; if(!type) return map.default;&#10; const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; if(map[norm]) return map[norm];&#10; const keys = Object.keys(map).filter(k =&gt; k !== 'default');&#10; for(const k of keys){&#10; if(k.includes(norm) || norm.includes(k)) return map[k];&#10; }&#10; return map.default;&#10;}&#10;&#10;function Clock() {&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10; const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10; return () =&gt; clearInterval(id);&#10; }, []);&#10; const hh = String(now.getHours()).padStart(2,'0');&#10; const mm = String(now.getMinutes()).padStart(2,'0');&#10; const ss = String(now.getSeconds()).padStart(2,'0');&#10; return (&#10; &lt;div className=&quot;aff-quais__clock&quot;&gt;&#10; &lt;div className=&quot;aff-quais__clock-box&quot;&gt;&lt;span className=&quot;aff-quais__clock-main&quot;&gt;{hh}:{mm}:{ss}&lt;/span&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesArriveesQuais(){&#10; const typeLogos = useTypeLogos();&#10;&#10; const rightRef = useRef(null);&#10; const stopsListRef = useRef(null);&#10Line truncated
</PendingDiffInfo>
</value>
</entry>
<entry key="$PROJECT_DIR$/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js" />
<option name="originalContent" value="import React, { useEffect, useState, useRef, useCallback } from 'react';&#10;import './ClassiquesDepartsQuais.css';&#10;import supabase from '../../../../lib/supabase';&#10;&#10;// Hook pour récupérer la map des logos depuis /type/data.json&#10;function useTypeLogos(){&#10; const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });&#10; useEffect(() =&gt; {&#10; let mounted = true;&#10;&#10; const load = async () =&gt; {&#10; const candidates = ['/type/quais/data-color.json', '/type/data.json'];&#10; for(const url of candidates){&#10; try{&#10; const res = await fetch(url);&#10; if(!res.ok) continue;&#10; const data = await res.json();&#10; if(!mounted) return;&#10; const logos = {};&#10; if(Array.isArray(data.logos)){&#10; data.logos.forEach(item =&gt; {&#10; // item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path&#10; let file = item.file;&#10; if(!file &amp;&amp; item.path){&#10; const parts = String(item.path).split('/');&#10; file = parts[parts.length-1];&#10; }&#10; if(!file) return;&#10; const basename = file.replace(/\.[^/.]+$/, '');&#10; const semantic = basename.replace(/^logo-/, '');&#10; const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; // Prefer color logos in the 'quais' subfolder if data came from the quais file&#10; if(url.includes('/quais/')){&#10; logos[key] = `/type/quais/${file}`;&#10; } else {&#10; logos[key] = `/type/${file}`;&#10; }&#10; });&#10; }&#10; logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';&#10; setMap(logos);&#10; return;&#10; }catch(e){&#10; // try next candidate (ignore error and continue loop)&#10; }&#10; }&#10; // no candidate succeeded — keep fallback&#10; };&#10;&#10; load();&#10; return () =&gt; { mounted = false; };&#10; }, []);&#10; return map;&#10;}&#10;&#10;function getTypeLogo(type, TYPE_LOGOS_MAP){&#10; const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };&#10; if(!type) return map.default;&#10; const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; if(map[norm]) return map[norm];&#10; const keys = Object.keys(map).filter(k =&gt; k !== 'default');&#10; for(const k of keys){&#10; if(k.includes(norm) || norm.includes(k)) return map[k];&#10; }&#10; return map.default;&#10;}&#10;&#10;function Clock() {&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10; const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10; return () =&gt; clearInterval(id);&#10; }, []);&#10; const hh = String(now.getHours()).padStart(2,'0');&#10; const mm = String(now.getMinutes()).padStart(2,'0');&#10; const ss = String(now.getSeconds()).padStart(2,'0');&#10; return (&#10; &lt;div className=&quot;aff-quais__clock&quot;&gt;&#10; &lt;div className=&quot;aff-quais__clock-box&quot;&gt;&lt;span className=&quot;aff-quais__clock-main&quot;&gt;{hh}:{mm}:{ss}&lt;/span&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesDepartsQuais(){&#10; const typeLogos = useTypeLogos();&#10;&#10; const rightRef = useRef(null);&#10; const stopsListRef = useRef(null);&#10; const railRef = useRef(null);&#10;&#10; // fallback mock strict (utilisé uniquement si aucune donnée réelle n'est trouvée)&#10; const mock = {&#10; time: '16h44',&#10; status: &quot;à l'heure&quot;,&#10; trainType: 'Fluo',&#10; trainNumber: '891811',&#10; destinationBig: 'Seurre',&#10; destinationStation: '',&#10; stops: [&#10; 'Ouges','Saulon','Longecourt','Aiserey','Brazey-en-Plaine','Saint-Jean-de-Losne','Chaugey','Pagny','Seurre','Mervans','Louhans','Saint-Amour','Bourg-en-Bresse'&#10; ],&#10; };&#10;&#10; // état pour l'horaire sélectionné&#10; const [selectedHoraire, setSelectedHoraire] = useState(null);&#10; const [loadingHoraire, setLoadingHoraire] = useState(false);&#10; const [horaireError, setHoraireError] = useState(null);&#10; const [candidatesCount, setCandidatesCount] = useState(0);&#10; // perturbations/travaux (récupérées depuis Supabase)&#10; const [perturbations, setPerturbations] = useState([]);&#10; const [, setTravaux] = useState([]);&#10;&#10; // helpers locaux (simples) — similaires à ceux du composant principal&#10; const normalizeName = useCallback((s) =&gt; {&#10; if (!s) return '';&#10; try { return String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim().replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (eLine truncated
<option name="updatedContent" value="import React, { useEffect, useState, useRef, useCallback } from 'react';&#10;import './ClassiquesDepartsQuais.css';&#10;import supabase from '../../../../lib/supabase';&#10;&#10;// Hook pour récupérer la map des logos depuis /type/data.json&#10;function useTypeLogos(){&#10; const [map, setMap] = useState({ default: '/type/quais/logo-sncf-color.svg' });&#10; useEffect(() =&gt; {&#10; let mounted = true;&#10;&#10; const load = async () =&gt; {&#10; const candidates = ['/type/quais/data-color.json', '/type/data.json'];&#10; for(const url of candidates){&#10; try{&#10; const res = await fetch(url);&#10; if(!res.ok) continue;&#10; const data = await res.json();&#10; if(!mounted) return;&#10; const logos = {};&#10; if(Array.isArray(data.logos)){&#10; data.logos.forEach(item =&gt; {&#10; // item.file is expected (ex: logo-tgv.svg). If absent, try to extract from path&#10; let file = item.file;&#10; if(!file &amp;&amp; item.path){&#10; const parts = String(item.path).split('/');&#10; file = parts[parts.length-1];&#10; }&#10; if(!file) return;&#10; const basename = file.replace(/\.[^/.]+$/, '');&#10; const semantic = basename.replace(/^logo-/, '');&#10; const key = semantic.toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; // Prefer color logos in the 'quais' subfolder if data came from the quais file&#10; if(url.includes('/quais/')){&#10; logos[key] = `/type/quais/${file}`;&#10; } else {&#10; logos[key] = `/type/${file}`;&#10; }&#10; });&#10; }&#10; logos.default = logos['SNCF-VOYAGEURS-LOGO'] || logos['SNFC-VOYAGEURS-LOGO'] || logos['SNCF'] || '/type/sncf-voyageurs-logo.png';&#10; setMap(logos);&#10; return;&#10; }catch(e){&#10; // try next candidate (ignore error and continue loop)&#10; }&#10; }&#10; // no candidate succeeded — keep fallback&#10; };&#10;&#10; load();&#10; return () =&gt; { mounted = false; };&#10; }, []);&#10; return map;&#10;}&#10;&#10;function getTypeLogo(type, TYPE_LOGOS_MAP){&#10; const map = TYPE_LOGOS_MAP || { default: '/type/sncf-voyageurs-logo.png' };&#10; if(!type) return map.default;&#10; const norm = String(type).toUpperCase().replace(/[^A-Z0-9]+/g, '-');&#10; if(map[norm]) return map[norm];&#10; const keys = Object.keys(map).filter(k =&gt; k !== 'default');&#10; for(const k of keys){&#10; if(k.includes(norm) || norm.includes(k)) return map[k];&#10; }&#10; return map.default;&#10;}&#10;&#10;function Clock() {&#10; const [now, setNow] = useState(new Date());&#10; useEffect(() =&gt; {&#10; const id = setInterval(() =&gt; setNow(new Date()), 1000);&#10; return () =&gt; clearInterval(id);&#10; }, []);&#10; const hh = String(now.getHours()).padStart(2,'0');&#10; const mm = String(now.getMinutes()).padStart(2,'0');&#10; const ss = String(now.getSeconds()).padStart(2,'0');&#10; return (&#10; &lt;div className=&quot;aff-quais__clock&quot;&gt;&#10; &lt;div className=&quot;aff-quais__clock-box&quot;&gt;&lt;span className=&quot;aff-quais__clock-main&quot;&gt;{hh}:{mm}:{ss}&lt;/span&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; );&#10;}&#10;&#10;export default function ClassiquesDepartsQuais(){&#10; const typeLogos = useTypeLogos();&#10;&#10; const rightRef = useRef(null);&#10; const stopsListRef = useRef(null);&#10; const railRef = useRef(null);&#10;&#10; // fallback mock strict (utilisé uniquement si aucune donnée réelle n'est trouvée)&#10; const mock = {&#10; time: '16h44',&#10; status: &quot;à l'heure&quot;,&#10; trainType: 'Fluo',&#10; trainNumber: '891811',&#10; destinationBig: 'Seurre',&#10; destinationStation: '',&#10; stops: [&#10; 'Ouges','Saulon','Longecourt','Aiserey','Brazey-en-Plaine','Saint-Jean-de-Losne','Chaugey','Pagny','Seurre','Mervans','Louhans','Saint-Amour','Bourg-en-Bresse'&#10; ],&#10; };&#10;&#10; // état pour l'horaire sélectionné&#10; const [selectedHoraire, setSelectedHoraire] = useState(null);&#10; const [loadingHoraire, setLoadingHoraire] = useState(false);&#10; const [horaireError, setHoraireError] = useState(null);&#10; const [candidatesCount, setCandidatesCount] = useState(0);&#10; // perturbations/travaux (récupérées depuis Supabase)&#10; const [perturbations, setPerturbations] = useState([]);&#10; const [, setTravaux] = useState([]);&#10;&#10; // helpers locaux (simples) — similaires à ceux du composant principal&#10; const normalizeName = useCallback((s) =&gt; {&#10; if (!s) return '';&#10; try { return String(s).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().trim().replace(/[-_]+/g,' ').replace(/\s+/g,' '); } catch (e)Line truncated
</PendingDiffInfo>
</value>
</entry>
</map>
</option>
</component>