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
+1548 -542

No files matched your search

+53 -43
View File
@@ -1,70 +1,80 @@
# Getting Started with Create React App # 🚅 Afficheurs Ferrovia
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). Application web de téléaffichage ferroviaire et d'information voyageurs en temps réel (PIDS - Passenger Information Display Systems), reproduisant fidèlement les écrans d'affichage des gares françaises (SNCF).
## Available Scripts ---
In the project directory, you can run: ## ✨ Fonctionnalités
### `npm start` - **📡 Données Réelles & Complètes :**
- Horaires théoriques issus de l'archive officielle **GTFS SNCF** (3 507 gares, 41 847 circulations : TGV INOUI, OUIGO, TER, INTERCITÉS, ICE, TGV Lyria).
- Retards, perturbations et suppressions en direct via **GTFS-RT**.
- Détection et affichage des **voies et quais réels** via les flux **SIRI-Lite**.
- **🖥️ Multiples Générations d'Afficheurs :**
- **AFL (Afficheurs Fluides & Lumineux) :** Écrans modernes pour halls et quais (Départs / Arrivées).
- **Classiques :** Écrans d'affichage historiques (Départs, Arrivées, Écrans de Quai avec liste des arrêts desservis).
- **EVA :** Écrans vidéo voyageurs enrichis.
- **🔊 Module d'Annonces Sonores :** Jingles et annonces synthétiques en temps réel.
- **🔍 Recherche Intelligente des Gares :** Reconnaissance instantanée des gares, codes UIC, variantes d'écriture et abréviations (`Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc.).
Runs the app in the development mode.\ ---
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\ ## 📋 Patchnotes / Historique des Versions
You may also see any lint errors in the console.
### `npm test` Pour consulter le journal détaillé de toutes les versions, consultez le fichier **[CHANGELOG.md](./CHANGELOG.md)**.
Launches the test runner in the interactive watch mode.\ <details open>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. <summary><h3>🚀 Version 4.0.0 (V4) - <em>Dernière version</em></h3></summary>
### `npm run build` - **Intégration GTFS & GTFS-RT Exhaustive :** 3 507 gares et 41 847 circulations réelles intégrées sans données mockées.
- **Quais Réels SIRI-Lite :** Extraction en continu des quais réels avec isolation stricte par gare sélectionnée et par arrêt.
- **Recherche Intelligente :** Algorithme tolérant aux tirets, accents et alias pour tous les noms de gares.
- **Normalisation des Horaires :** Prise en charge des trains de nuit et horaires modulo 24h.
- **Codebase Propre :** Résolution complète de tous les avertissements ESLint (`Compiled successfully`).
Builds the app for production to the `build` folder.\ </details>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\ <details>
Your app is ready to be deployed! <summary><h3>🚆 Version 3.0.0 (V3)</h3></summary>
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - Nouveaux types d'afficheurs AFL, Classiques et EVA.
- Module d'annonces sonores en temps réel.
- Gestion avancée des trains combinés / multi-tranches et des substitutions par autocar.
### `npm run eject` </details>
**Note: this is a one-way operation. Once you `eject`, you can't go back!** <details>
<summary><h3>🚉 Version 2.0.0 (V2)</h3></summary>
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - Écrans dédiés par voie et quai avec liste déroulante des arrêts aval.
- Alternance dynamique des états de circulation et synchronisation automatique.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. </details>
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. <details>
<summary><h3>🏁 Version 1.0.0 (V1)</h3></summary>
## Learn More - Lancement initial de la plateforme avec horloge temps réel et reproduction de la signalétique ferroviaire.
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). </details>
To learn React, check out the [React documentation](https://reactjs.org/). ---
### Code Splitting ## 🛠️ Installation & Démarrage
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) ```bash
# Installer les dépendances
npm install
### Analyzing the Bundle Size # Lancer le serveur de développement (http://localhost:3000)
npm start
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) # Compiler pour la production
npm run build
```
### Making a Progressive Web App ---
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) ## 📄 Licence
### Advanced Configuration Projet développé pour la simulation et la restitution d'écrans d'information ferroviaire. Données fournies sous licence ouverte via transport.data.gouv.fr et SNCF Open Data.
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
+347 -3
View File
@@ -16,6 +16,8 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"autoprefixer": "^10.4.27", "autoprefixer": "^10.4.27",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"gtfs-realtime-bindings": "^2.2.0",
"protobufjs": "^8.8.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
@@ -2872,6 +2874,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jsdoc/salty": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz",
"integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.18.1"
},
"engines": {
"node": ">=v12.0.0"
}
},
"node_modules/@leichtgewicht/ip-codec": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -3778,6 +3792,28 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
"integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -5606,6 +5642,18 @@
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/catharsis": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -8872,6 +8920,18 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/gtfs-realtime-bindings": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/gtfs-realtime-bindings/-/gtfs-realtime-bindings-2.2.0.tgz",
"integrity": "sha512-JX6aADdPWtKMXA3Z/G2TK2kgR/gXZFFpfL7cdlR2fYvnJK5JDiyfEGC4+/65dF591d8nooUjhM387OSdesAIDQ==",
"dependencies": {
"protobufjs": "^8.2.1",
"protobufjs-cli": "^2.2.1"
},
"engines": {
"node": ">=22"
}
},
"node_modules/gzip-size": { "node_modules/gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -11079,12 +11139,71 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/js2xmlparser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
"integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
"license": "Apache-2.0",
"dependencies": {
"xmlcreate": "^2.0.4"
}
},
"node_modules/jsbn": { "node_modules/jsbn": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsdoc": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz",
"integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==",
"license": "Apache-2.0",
"dependencies": {
"@babel/parser": "^7.20.15",
"@jsdoc/salty": "^0.2.1",
"@types/markdown-it": "^14.1.1",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.2",
"klaw": "^3.0.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"marked": "^4.0.10",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
"strip-json-comments": "^3.1.0",
"underscore": "~1.13.2"
},
"bin": {
"jsdoc": "jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/jsdoc/node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jsdoc/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jsdom": { "node_modules/jsdom": {
"version": "16.7.0", "version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -11283,6 +11402,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/klaw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.1.9"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -11366,6 +11494,25 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/linkify-it": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
@@ -11406,9 +11553,9 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
@@ -11447,6 +11594,12 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11528,6 +11681,73 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/markdown-it": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz",
"integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.5.0",
"linkify-it": "^5.0.2",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-anchor": {
"version": "8.6.7",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
"license": "Unlicense",
"peerDependencies": {
"@types/markdown-it": "*",
"markdown-it": "*"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/marked": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -11543,6 +11763,12 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"license": "MIT"
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -13818,6 +14044,85 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/protobufjs": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz",
"integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==",
"license": "BSD-3-Clause",
"dependencies": {
"long": "^5.3.2"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/protobufjs-cli": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-2.7.0.tgz",
"integrity": "sha512-wjrZPibf/U73dDJMh9jCqjyeDH7t6+OMD16vKp/Z1CY7yyVE/pX5a9EvCq89p3ADHo+KyaLknA5lxyaaM252KA==",
"license": "BSD-3-Clause",
"dependencies": {
"escodegen": "^2.0.0",
"espree": "^9.6.1",
"estraverse": "^5.1.0",
"glob": "^8.1.0",
"jsdoc": "^4.0.5",
"minimist": "^1.2.8",
"tmp": "^0.2.7"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts",
"protoc-gen-pbjs": "bin/protoc-gen-pbjs"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"protobufjs": "^8.8.0"
}
},
"node_modules/protobufjs-cli/node_modules/brace-expansion": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/protobufjs-cli/node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/protobufjs-cli/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -13861,6 +14166,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/q": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -14527,6 +14841,15 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/requizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
"integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21"
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -16330,6 +16653,15 @@
"@popperjs/core": "^2.9.0" "@popperjs/core": "^2.9.0"
} }
}, },
"node_modules/tmp": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/tmpl": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -16626,6 +16958,12 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -17772,6 +18110,12 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/xmlcreate": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
"integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
"license": "Apache-2.0"
},
"node_modules/xstate": { "node_modules/xstate": {
"version": "4.38.3", "version": "4.38.3",
"resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz", "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz",
+2
View File
@@ -11,6 +11,8 @@
"autoprefixer": "^10.4.27", "autoprefixer": "^10.4.27",
"dependencies": "^0.0.1", "dependencies": "^0.0.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"gtfs-realtime-bindings": "^2.2.0",
"protobufjs": "^8.8.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
+106 -77
View File
@@ -8,8 +8,7 @@ import Displays from './pages/Displays';
import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts'; import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts';
import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees'; import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees';
import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais'; import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais';
import sessionMemory from './lib/sessionMemory'; import ClassiquesArriveesQuais from './pages/afficheurs/classiques/quais/ClassiquesArriveesQuais';
import ClassiquesArriveesQuais from "./pages/afficheurs/classiques/quais/ClassiquesArriveesQuais";
import AFLDeparts from './pages/afficheurs/afl/AFLDeparts'; import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees'; import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais'; import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
@@ -18,19 +17,19 @@ import EVAArrivees from './pages/afficheurs/eva/EVAArrivees';
import EVADeparts from './pages/afficheurs/eva/EVADeparts'; import EVADeparts from './pages/afficheurs/eva/EVADeparts';
import SelectAfficheur from './pages/SelectAfficheur'; import SelectAfficheur from './pages/SelectAfficheur';
import AfficheurViewportWrapper from './components/AfficheurViewportWrapper'; import AfficheurViewportWrapper from './components/AfficheurViewportWrapper';
import sessionMemory from './lib/sessionMemory';
import { getSourceFromPath } from './services/railwayService';
applyPolyfills().then(() => defineCustomElements(window)); applyPolyfills().then(() => defineCustomElements(window));
// Petit routeur client sans dépendances externes // Petit routeur client sans dépendances externes
function useSimpleRouter() { function useSimpleRouter() {
function normalizePath(p) { function normalizePath(p) {
if (!p) return '/'; if (!p) return '/';
// retirer query string et hash pour normaliser le chemin utilisé en internal route // retirer query string et hash
p = String(p).split(/[?#]/)[0]; p = String(p).split(/[?#]/)[0];
// traiter les cas comme /index.html
if (p.indexOf('index.html') !== -1) return '/'; if (p.indexOf('index.html') !== -1) return '/';
// supprimer espaces en début/fin
p = p.trim(); p = p.trim();
// supprimer slashs finaux sauf pour la racine
if (p !== '/') p = p.replace(/\/+$/, ''); if (p !== '/') p = p.replace(/\/+$/, '');
return p || '/'; return p || '/';
} }
@@ -49,24 +48,16 @@ function useSimpleRouter() {
function navigate(path) { function navigate(path) {
const target = normalizePath(path); const target = normalizePath(path);
// toujours pousser l'URL complète (path peut contenir query/hash),
// mais stocker en internal state la version normalisée (sans query/hash)
// comparer l'URL complète (pathname + search + hash) afin de permettre
// des navigations qui ne changent que la querystring (ex. ?gare=...)
const currentFull = window.location.pathname + (window.location.search || '') + (window.location.hash || ''); const currentFull = window.location.pathname + (window.location.search || '') + (window.location.hash || '');
if (currentFull !== path) { if (currentFull !== path) {
console.debug('[router] navigate ->', path, '(normalized ->', target, ')'); console.debug('[router] navigate ->', path, '(normalized ->', target, ')');
try { try {
window.history.pushState({}, '', path); window.history.pushState({}, '', path);
} catch (e) { } catch (e) {
// fallback si path est invalide try { window.history.pushState({}, '', target); } catch (err) {}
try { window.history.pushState({}, '', target); } catch (err) { /* ignore */ }
} }
// mettre à jour la route interne (sans query/hash)
setRoute(target); setRoute(target);
} else { } else {
// si l'URL complète est identique, forcer un update de state pour
// déclencher un rendu si besoin (rare)
setRoute(target); setRoute(target);
} }
} }
@@ -77,122 +68,160 @@ function useSimpleRouter() {
function App() { function App() {
const { route, navigate } = useSimpleRouter(); const { route, navigate } = useSimpleRouter();
const source = getSourceFromPath(route);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/');
// Composant Home explicite (panels cliquables) // Composant Home explicite (panels cliquables)
function Home({ navigate }) { function Home({ navigate, prefix, source }) {
useEffect(() => {
console.debug('[Home] monté, pathname =', window.location.pathname);
}, []);
const panelStyle = { const panelStyle = {
width: '40%', width: '42%',
cursor: 'pointer', cursor: 'pointer',
padding: 24, padding: 24,
borderRadius: 8, borderRadius: 10,
boxShadow: '0 2px 8px rgba(0,0,0,0.12)', boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
background: '#fff', background: '#fff',
textAlign: 'center' textAlign: 'center',
border: '1px solid #e0e8f6',
transition: 'transform 0.15s ease'
}; };
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '40px', alignItems: 'center', background: '#e9f0ff', paddingTop: 32, paddingBottom: 32 }}> <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '32px 24px', alignItems: 'center', background: '#e9f0ff', borderRadius: '12px' }}>
<div style={{ width: '100%', textAlign: 'center', marginBottom: 16 }}> <div style={{ width: '100%', textAlign: 'center', marginBottom: 20 }}>
<h1 style={{ margin: 0, fontSize: 28 }}>Accueil</h1> <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' }}>
<p style={{ margin: '8px 0 20px', color: '#333' }}>Choisissez une section ci-dessous</p> {source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%' }}> <h1 style={{ margin: '8px 0', fontSize: 28, color: '#002257' }}>Accueil</h1>
<div <p style={{ margin: '4px 0 16px', color: '#555' }}>
role="button" {source === 'supabase'
tabIndex={0} ? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); }} : "Consultez les afficheurs connectés aux flux réels GTFS et Open Data SNCF."}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); })() : null)} </p>
style={panelStyle}
aria-label="Accéder aux départs"
>
<h2>Départs</h2>
<p>Consultez les prochains départs d'une gare. Cliquez pour choisir une gare et voir les horaires.</p>
</div> </div>
<div <div style={{ display: 'flex', justifyContent: 'space-around', width: '100%', maxWidth: '800px' }}>
role="button" <div
tabIndex={0} role="button"
onClick={() => { try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); }} tabIndex={0}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); })() : null)} onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); }}
style={panelStyle} onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); })() : null)}
aria-label="Accéder aux arrivées" style={panelStyle}
> aria-label="Accéder aux départs"
<h2>Arrivées</h2> >
<p>Consultez les prochaines arrivées d'une gare. Cliquez pour choisir une gare et voir les informations.</p> <h2 style={{ color: '#00439e', marginTop: 0 }}>🚆 Départs</h2>
</div> <p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.</p>
</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>
</div>
</div> </div>
</div> </div>
); );
} }
// Routes pour afficheurs : format /afficheurs/{mode}/{type}?gare=... // Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
// route plus spécifique pour l'afficheur "quais" if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) {
if (route.startsWith('/afficheurs/departs/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesDepartsQuais /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><ClassiquesDepartsQuais /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/arrivees/classiques/quais')) { if (cleanRoute.startsWith('/afficheurs/arrivees/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesArriveesQuais /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><ClassiquesArriveesQuais /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/departs/classiques')) { if (cleanRoute.startsWith('/afficheurs/departs/classiques')) {
return <AfficheurViewportWrapper><ClassiquesDeparts /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><ClassiquesDeparts /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/departs/afl/quais')) { if (cleanRoute.startsWith('/afficheurs/departs/afl/quais')) {
return <AfficheurViewportWrapper><AFLDepartsQuais /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><AFLDepartsQuais /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/departs/afl')) { if (cleanRoute.startsWith('/afficheurs/departs/afl')) {
return <AfficheurViewportWrapper><AFLDeparts /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><AFLDeparts /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/departs/eva')) { if (cleanRoute.startsWith('/afficheurs/departs/eva')) {
return <AfficheurViewportWrapper><EVADeparts /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><EVADeparts /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/arrivees/afl/quais')) { if (cleanRoute.startsWith('/afficheurs/arrivees/afl/quais')) {
return <AfficheurViewportWrapper><AFLArriveesQuais /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><AFLArriveesQuais /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/arrivees/afl')) { if (cleanRoute.startsWith('/afficheurs/arrivees/afl')) {
return <AfficheurViewportWrapper><AFLArrivees /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><AFLArrivees /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/arrivees/eva')) { if (cleanRoute.startsWith('/afficheurs/arrivees/eva')) {
return <AfficheurViewportWrapper><EVAArrivees /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><EVAArrivees /></AfficheurViewportWrapper>;
} }
if (route.startsWith('/afficheurs/arrivees/classiques')) { if (cleanRoute.startsWith('/afficheurs/arrivees/classiques')) {
return <AfficheurViewportWrapper><ClassiquesArrivees /></AfficheurViewportWrapper>; return <AfficheurViewportWrapper><ClassiquesArrivees /></AfficheurViewportWrapper>;
} }
let content; let content;
if (route === '/departs') { if (cleanRoute === '/departs') {
content = <Departs onBack={() => navigate('/')} navigate={navigate} />; content = <Departs onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (route === '/arrivees') { } else if (cleanRoute === '/arrivees') {
content = <Arrivees onBack={() => navigate('/')} navigate={navigate} />; content = <Arrivees onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (route === '/select-afficheur') { } else if (cleanRoute === '/select-afficheur') {
content = <SelectAfficheur onBack={() => { content = <SelectAfficheur onBack={() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const gare = params.get('gare') || ''; const gare = params.get('gare') || '';
const mode = params.get('mode') || 'departs'; const mode = params.get('mode') || 'departs';
navigate(`/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`); navigate(`${prefix}/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
}} navigate={navigate} />; }} navigate={navigate} />;
} else if (route.startsWith('/afficheurs')) { } else if (cleanRoute.startsWith('/afficheurs')) {
content = <Displays onBack={() => navigate('/')} navigate={navigate} />; content = <Displays onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else { } else {
content = <Home navigate={navigate} />; content = <Home navigate={navigate} prefix={prefix} source={source} />;
} }
return ( return (
<div className="sncf-voyageurs" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> <div className="sncf-voyageurs" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<wcs-galactic app-name="Affichage Ferroviaire" version="1.0.0"> <wcs-galactic app-name="Affichage Ferroviaire" version="1.0.0">
<wcs-galactic-menu-item> <wcs-galactic-menu-item>
<a href="/departs" onClick={(e) => { e.preventDefault(); navigate('/departs'); }}>Départs</a> <a href={`${prefix}/departs`} onClick={(e) => { e.preventDefault(); navigate(`${prefix}/departs`); }}>Départs</a>
</wcs-galactic-menu-item> </wcs-galactic-menu-item>
<wcs-galactic-menu-item> <wcs-galactic-menu-item>
<a href="/arrivees" onClick={(e) => { e.preventDefault(); navigate('/arrivees'); }}>Arrivées</a> <a href={`${prefix}/arrivees`} onClick={(e) => { e.preventDefault(); navigate(`${prefix}/arrivees`); }}>Arrivées</a>
</wcs-galactic-menu-item>
<wcs-galactic-menu-item>
{source === 'supabase' ? (
<a
href="/"
onClick={(e) => { e.preventDefault(); navigate('/'); }}
style={{ color: '#008b47', fontWeight: 600 }}
title="Basculer vers le mode SNCF GTFS"
>
🌐 Passer en mode SNCF Live
</a>
) : (
<a
href="/ferrovia"
onClick={(e) => { e.preventDefault(); navigate('/ferrovia'); }}
style={{ color: '#00439e', fontWeight: 600 }}
title="Basculer vers la Base de Données Supabase"
>
🗄️ Passer en mode BDD Ferrovia
</a>
)}
</wcs-galactic-menu-item> </wcs-galactic-menu-item>
</wcs-galactic> </wcs-galactic>
<header style={{ padding: 12, background: '#00439e', color: '#fff' }}> <header style={{ padding: 12, background: source === 'supabase' ? '#003366' : '#00439e', color: '#fff', transition: 'background 0.3s' }}>
<div style={{ maxWidth: 1000, margin: '0 auto' }}> <div style={{ maxWidth: 1000, margin: '0 auto', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1 style={{ margin: 0, fontSize: 20 }}>Affichage Ferroviaire — Accueil</h1> <div>
<div style={{ fontSize: 12, opacity: 0.9 }}>Route actuelle: {route}</div> <h1 style={{ margin: 0, fontSize: 20 }}>
{source === 'supabase' ? 'Affichage Ferroviaire — BDD Supabase (/ferrovia)' : 'Affichage Ferroviaire — SNCF GTFS / Temps Réel'}
</h1>
<div style={{ fontSize: 12, opacity: 0.9 }}>Route actuelle: {route}</div>
</div>
<div style={{ fontSize: '13px', background: 'rgba(255,255,255,0.15)', padding: '6px 12px', borderRadius: '6px' }}>
Source: <strong>{source === 'supabase' ? 'Supabase Database' : 'SNCF Open Data / GTFS'}</strong>
</div>
</div> </div>
</header> </header>
+123 -59
View File
@@ -1,102 +1,168 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import supabase from '../lib/supabase';
import sessionMemory from '../lib/sessionMemory'; import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Arrivees({ onBack, navigate }) { export default function Arrivees({ onBack, navigate }) {
const [gares, setGares] = useState([]); const source = getSourceFromPath(window.location.pathname);
const [loading, setLoading] = useState(true); 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 [error, setError] = useState(null);
const [selected, setSelected] = useState(''); const [selected, setSelected] = useState('');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
if (source === 'supabase') {
async function loadGares() { async function loadSupabaseGares() {
setLoading(true); setLoading(true);
setError(null); setError(null);
if (!supabase) { try {
if (mounted) { const data = await searchGares('', 'supabase');
setError( if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
"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)." } catch (err) {
); console.error('Supabase error:', err);
setLoading(false); 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; return;
} }
try { const timer = setTimeout(async () => {
const { data, error } = await supabase.from('gares').select('nom'); try {
if (error) throw error; const results = await searchGares(q, 'sncf');
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []); if (active) setSncfResults(results);
} catch (err) { } catch (e) {
console.error('Supabase error:', err); if (active) setSncfResults([]);
if (mounted) setError(err.message || String(err)); }
} finally { }, 200);
if (mounted) setLoading(false);
} 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 () => { const handleSelectGare = (nom) => {
mounted = false; setSelected(nom);
}; try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
}, []); if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`);
}
};
return ( return (
<div style={{ padding: 20 }}> <div style={{ padding: 20 }}>
<h1>Arrivées</h1> <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>
{loading && <p>Chargement des gares...</p>} {loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>} {error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && ( {!loading && !error && (
<div> <div>
{/* Barre de recherche centrée */} {/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input <input
type="search" type="search"
placeholder="Rechercher une gare..." placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare" aria-label="Rechercher une gare"
autoFocus
/> />
</div> </div>
{/* Résultats sous forme de lignes */} {/* Invitation à la recherche en mode SNCF */}
<div role="list" aria-label="Résultats des gares"> {source === 'sncf' && search.trim().length < 2 && (
{gares <div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase())) <p style={{ margin: 0, fontSize: '15px' }}>
.map((nom) => ( 🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
<div key={nom} role="listitem"> </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 <div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', cursor: 'pointer' }}
onClick={() => { onClick={() => handleSelectGare(nom)}
setSelected(nom); onKeyDown={(e) => (e.key === 'Enter' ? handleSelectGare(nom) : null)}
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} tabIndex={0}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`); >
}} <div>
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`) : setSelected(nom)) : null)} <strong style={{ fontSize: '15px' }}>{nom}</strong>
tabIndex={0} {g.commune && <span style={{ marginLeft: '8px', color: '#666', fontSize: '13px' }}>({g.commune} - {g.departement})</span>}
> </div>
<div>{nom}</div> <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>} {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>
<div style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} /> );
</div> })}
))} </div>
</div> )}
{selected && ( {selected && (
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p> <p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => { <button onClick={() => {
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) { if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`); navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
} else {
alert(`Afficher les arrivées pour ${selected}`);
} }
}}>Voir les arrivées</button> }}>Voir les arrivées</button>
</div> </div>
@@ -110,5 +176,3 @@ export default function Arrivees({ onBack, navigate }) {
</div> </div>
); );
} }
+124 -61
View File
@@ -1,103 +1,168 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import supabase from '../lib/supabase';
import sessionMemory from '../lib/sessionMemory'; import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Departs({ onBack, navigate }) { export default function Departs({ onBack, navigate }) {
const [gares, setGares] = useState([]); const source = getSourceFromPath(window.location.pathname);
const [loading, setLoading] = useState(true); 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 [error, setError] = useState(null);
const [selected, setSelected] = useState(''); const [selected, setSelected] = useState('');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
if (source === 'supabase') {
async function loadGares() { async function loadSupabaseGares() {
setLoading(true); setLoading(true);
setError(null); setError(null);
if (!supabase) { try {
// Le client Supabase n'a pas pu être initialisé (variables d'env manquantes) const data = await searchGares('', 'supabase');
if (mounted) { if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
setError( } catch (err) {
"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)." console.error('Supabase error:', err);
); if (mounted) setError(err.message || String(err));
setLoading(false); } 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; return;
} }
try { const timer = setTimeout(async () => {
const { data, error } = await supabase.from('gares').select('nom'); try {
if (error) throw error; const results = await searchGares(q, 'sncf');
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []); if (active) setSncfResults(results);
} catch (err) { } catch (e) {
console.error('Supabase error:', err); if (active) setSncfResults([]);
if (mounted) setError(err.message || String(err)); }
} finally { }, 200);
if (mounted) setLoading(false);
} 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 () => { const handleSelectGare = (nom) => {
mounted = false; setSelected(nom);
}; try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
}, []); if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}
};
return ( return (
<div style={{ padding: 20 }}> <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>} {loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>} {error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && ( {!loading && !error && (
<div> <div>
{/* Barre de recherche centrée */} {/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input <input
type="search" type="search"
placeholder="Rechercher une gare..." placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }} style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare" aria-label="Rechercher une gare"
autoFocus
/> />
</div> </div>
{/* Résultats sous forme de lignes */} {/* Invitation à la recherche en mode SNCF */}
<div role="list" aria-label="Résultats des gares"> {source === 'sncf' && search.trim().length < 2 && (
{gares <div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase())) <p style={{ margin: 0, fontSize: '15px' }}>
.map((nom) => ( 🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
<div key={nom} role="listitem"> </p>
<div </div>
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }} )}
onClick={() => {
setSelected(nom); {/* Aucun résultat */}
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} {source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`); <div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
}} Aucune gare SNCF trouvée pour "{search}".
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`) : setSelected(nom)) : null)} </div>
tabIndex={0} )}
>
<div>{nom}</div> {/* Résultats */}
{selected === nom && <div style={{ color: '#0070f3' }}>Sélectionnée</div>} {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>
<div style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} /> );
</div> })}
))} </div>
</div> )}
{selected && ( {selected && (
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p> <p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => { <button onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) { if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=departs`); navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
} else {
alert(`Afficher les départs pour ${selected}`);
} }
}}>Voir les départs</button> }}>Voir les départs</button>
</div> </div>
@@ -111,5 +176,3 @@ export default function Departs({ onBack, navigate }) {
</div> </div>
); );
} }
+40 -32
View File
@@ -1,8 +1,12 @@
import React from 'react'; import React from 'react';
import WcsButton from '../components/WcsButton'; import WcsButton from '../components/WcsButton';
import sessionMemory from '../lib/sessionMemory'; import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath } from '../services/railwayService';
export default function Displays({ onBack, navigate }) { export default function Displays({ onBack, navigate }) {
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const types = [ const types = [
{ id: 'classiques', label: 'Classiques' }, { id: 'classiques', label: 'Classiques' },
{ id: 'eva', label: 'EVA' }, { id: 'eva', label: 'EVA' },
@@ -10,25 +14,23 @@ export default function Displays({ onBack, navigate }) {
{ id: 'transilien', label: 'TRANSILIEN' } { id: 'transilien', label: 'TRANSILIEN' }
]; ];
// Récupérer la gare : soit dans le pathname (/afficheurs/<gare>), // Nettoyage du chemin pour extraire la gare même avec /ferrovia
// soit via la querystring ?gare=... si la route est déjà /afficheurs/<type>/<mode> const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/^\/ferrovia/, '');
const parts = typeof window !== 'undefined' ? window.location.pathname.split('/').filter(Boolean) : []; const parts = cleanPath.split('/').filter(Boolean);
const typeIds = types.map(t => t.id); const typeIds = types.map(t => t.id);
let gare = null; let gare = null;
if (parts[0] === 'afficheurs' && parts[1] && !typeIds.includes(parts[1])) { if (parts[0] === 'afficheurs' && parts[1] && !typeIds.includes(parts[1])) {
// cas /afficheurs/<gare>
try { try {
gare = decodeURIComponent(parts.slice(1).join('/')); gare = decodeURIComponent(parts.slice(1).join('/'));
} catch (e) { } catch (e) {
gare = parts.slice(1).join('/'); gare = parts.slice(1).join('/');
} }
} else { } else {
// si l'URL est /afficheurs/<type>/<mode>, regarder dans la querystring ?gare=...
const paramsForGare = new URLSearchParams(window.location.search || ''); const paramsForGare = new URLSearchParams(window.location.search || '');
const g = paramsForGare.get('gare'); const g = paramsForGare.get('gare');
if (g) { if (g) {
try { try {
// décoder puis remplacer les tirets par des espaces pour l'affichage
gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' '); gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' ');
} catch (e) { } catch (e) {
gare = String(g).replace(/-/g, ' '); gare = String(g).replace(/-/g, ' ');
@@ -36,37 +38,52 @@ export default function Displays({ onBack, navigate }) {
} }
} }
// déterminer le mode (departs/arrivees) : querystring > localStorage > default('departs') // Déterminer le mode (departs/arrivees)
const params = new URLSearchParams(window.location.search || ''); const params = new URLSearchParams(window.location.search || '');
let mode = params.get('mode'); let mode = params.get('mode');
if (!mode) { if (!mode) {
try { try {
const stored = sessionMemory.get('afficheurMode'); const stored = sessionMemory.get('afficheurMode');
if (stored) mode = stored; if (stored) mode = stored;
} catch (e) { } catch (e) {}
// ignore
}
} }
if (!mode) mode = 'departs'; if (!mode) mode = 'departs';
return ( return (
<div style={{ padding: 20 }}> <div style={{ padding: 20 }}>
<h1>Choisissez un type d'afficheur</h1> <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>
{gare && <p>Gare sélectionnée : <strong>{gare}</strong></p>} {gare && <p>Gare sélectionnée : <strong>{gare}</strong></p>}
<p>Mode sélectionné : <strong>{mode === 'arrivees' ? 'Arrivées' : 'Départs'}</strong></p> <p>Mode sélectionné : <strong>{mode === 'arrivees' ? 'Arrivées' : 'Départs'}</strong></p>
{/* Boutons directs pour afficher les afficheurs classiques supprimés */}
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16 }}> <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16 }}>
{types.map((t) => ( {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={() => { <div
if (typeof navigate === 'function') { key={t.id}
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : ''; role="button"
navigate(`/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`); tabIndex={0}
return; style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }}
} onClick={() => {
alert(`${t.label} pour ${gare || '—'}`); 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> <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> <p style={{ margin: 0, color: '#555' }}>Afficher le style {t.label} pour la gare {gare || ''}.</p>
</div> </div>
@@ -74,29 +91,21 @@ export default function Displays({ onBack, navigate }) {
</div> </div>
<div style={{ marginTop: 24 }}> <div style={{ marginTop: 24 }}>
{/* Bouton de retour au style WCS SNCF */}
<WcsButton <WcsButton
// Styliser le bouton comme un bouton d'action WCS. On peut ajuster `mode` ou
// ajouter `className` (ex: "wcs-primary") si vous avez des classes globales.
mode="plain" mode="plain"
className="wcs-primary" className="wcs-primary"
onClick={() => { onClick={() => {
// Appelle le callback fourni par le routeur simple si disponible
try { try {
if (typeof onBack === 'function') { if (typeof onBack === 'function') {
onBack(); onBack();
return; return;
} }
} catch (e) { } catch (e) {}
// ignore
}
// Fallback : si l'historique du navigateur permet un back, on l'utilise
if (typeof window !== 'undefined' && window.history && window.history.length > 1) { if (typeof window !== 'undefined' && window.history && window.history.length > 1) {
window.history.back(); window.history.back();
} else { } else {
// Redirection sûre vers la racine window.location.href = prefix || '/';
window.location.href = '/';
} }
}} }}
label="Retour" label="Retour"
@@ -105,4 +114,3 @@ export default function Displays({ onBack, navigate }) {
</div> </div>
); );
} }
+32 -54
View File
@@ -1,8 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import WcsButton from '../components/WcsButton'; import WcsButton from '../components/WcsButton';
import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService';
export default function SelectAfficheur({ onBack, navigate }) { export default function SelectAfficheur({ onBack, navigate }) {
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const params = new URLSearchParams(window.location.search || ''); const params = new URLSearchParams(window.location.search || '');
const gareParam = params.get('gare') || ''; const gareParam = params.get('gare') || '';
const modeParam = params.get('mode') || 'departs'; const modeParam = params.get('mode') || 'departs';
@@ -21,50 +24,13 @@ export default function SelectAfficheur({ onBack, navigate }) {
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
async function loadPlatforms() { async function loadPlatforms() {
if (!supabase) {
setLoading(false);
return;
}
try { try {
setLoading(true); setLoading(true);
const { data, error } = await supabase.from('horaires').select('quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom, gares_desservies'); const sortedPlatforms = await getPlatformsForStation(decodedGare || gareParam, source);
if (error) throw error;
if (!mounted) return; if (!mounted) return;
const platformSet = new Set(); setPlatforms(sortedPlatforms || []);
const normGare = decodedGare.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim(); if (sortedPlatforms && sortedPlatforms.length > 0) {
(data || []).forEach(h => {
let stopsList = [];
if (Array.isArray(h.gares_desservies)) {
stopsList = h.gares_desservies;
} else if (typeof h.gares_desservies === 'string') {
try { stopsList = JSON.parse(h.gares_desservies); } catch (e) {}
}
if (!Array.isArray(stopsList)) stopsList = [];
let foundStop = stopsList.find(s => {
const name = (s.gare_nom || s.nom || s.name || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
return name === normGare || name.includes(normGare) || normGare.includes(name);
});
if (foundStop) {
const platform = (foundStop.quai_reel || foundStop.quai || foundStop.platform || foundStop.quay || '').toString().trim();
if (platform) platformSet.add(platform);
} else {
const origin = (h.gare_depart_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
const dest = (h.gare_arrivee_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
if (origin === normGare || origin.includes(normGare) || normGare.includes(origin) ||
dest === normGare || dest.includes(normGare) || normGare.includes(dest)) {
const platform = (h.quai_reel || h.voie || h.quai || '').toString().trim();
if (platform) platformSet.add(platform);
}
}
});
const sortedPlatforms = Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
setPlatforms(sortedPlatforms);
if (sortedPlatforms.length > 0) {
setSelectedPlatform(sortedPlatforms[0]); setSelectedPlatform(sortedPlatforms[0]);
} }
} catch (err) { } catch (err) {
@@ -78,12 +44,12 @@ export default function SelectAfficheur({ onBack, navigate }) {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [gareParam, decodedGare]); }, [gareParam, decodedGare, source]);
const handleGeneral = () => { const handleGeneral = () => {
if (typeof navigate === 'function') { if (typeof navigate === 'function') {
const kebab = gareParam ? String(gareParam).trim() : ''; const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`); navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`);
} }
}; };
@@ -95,7 +61,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
} }
if (typeof navigate === 'function') { if (typeof navigate === 'function') {
const kebab = gareParam ? String(gareParam).trim() : ''; const kebab = gareParam ? String(gareParam).trim() : '';
navigate(`/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`); navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}/quais?gare=${encodeURIComponent(kebab)}&quai=${encodeURIComponent(quai)}`);
} }
}; };
@@ -109,15 +75,27 @@ export default function SelectAfficheur({ onBack, navigate }) {
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
}}> }}>
<h2 style={{ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
marginTop: 0, <h2 style={{
marginBottom: '8px', margin: 0,
color: '#002257', color: '#002257',
fontSize: '28px', fontSize: '28px',
fontWeight: 700 fontWeight: 700
}}> }}>
Sélection de l'Afficheur Sélection de l'Afficheur
</h2> </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>
<p style={{ <p style={{
margin: '0 0 24px 0', margin: '0 0 24px 0',
color: '#555555', color: '#555555',
@@ -268,7 +246,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
if (typeof onBack === 'function') { if (typeof onBack === 'function') {
onBack(); onBack();
} else if (typeof navigate === 'function') { } else if (typeof navigate === 'function') {
navigate(`/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`); navigate(`${prefix}/afficheurs?gare=${encodeURIComponent(gareParam)}&mode=${modeParam}`);
} }
}} }}
label="Retour aux types d'afficheurs" label="Retour aux types d'afficheurs"
+80 -16
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -155,10 +168,17 @@ export default function AFLArrivees(){
useAnnonceSonoreRealtime(resolvedGareId, 'AFLArrivees'); useAnnonceSonoreRealtime(resolvedGareId, 'AFLArrivees');
useAutoAnnonce(horaires, currentTime, true); useAutoAnnonce(horaires, currentTime, true);
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -188,9 +208,7 @@ export default function AFLArrivees(){
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -315,11 +333,14 @@ export default function AFLArrivees(){
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
}, []); }, []);
@@ -626,18 +647,57 @@ export default function AFLArrivees(){
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; // empêche les fetchs concurrents let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted) return; if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé if (inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement'); setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false); setLoading(false);
@@ -1022,7 +1082,7 @@ export default function AFLArrivees(){
const intervalId = setInterval(fetchData, intervalMs); const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
// Helpers pour perturbations (memoized plus haut) // Helpers pour perturbations (memoized plus haut)
@@ -1301,6 +1361,10 @@ export default function AFLArrivees(){
<div className="afl-arr-voie-box-compact"> <div className="afl-arr-voie-box-compact">
<BusIcon size="52px" color="#173e31" /> <BusIcon size="52px" color="#173e31" />
</div> </div>
) : showPlatform ? (
<div className="afl-arr-voie-box-compact">
<div className="afl-arr-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null ) : null
)} )}
</div> </div>
+48 -9
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLArriveesQuais.css'; import './AFLArriveesQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo'; import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57) // Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`; return `${hh}:${mm}`;
} }
const parts = String(v).split(':'); const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (parts.length >= 2) { if (m) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`; const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
} }
return v; return v;
}; };
@@ -137,9 +140,12 @@ const AFLArriveesQuais = () => {
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/); const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
@@ -351,6 +357,39 @@ const AFLArriveesQuais = () => {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
const { data: sncfData } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees',
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const enriched = {
...train,
_rawTime: train.heure_arrivee || train.heure_depart,
_displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
_status: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null
},
_nextStationStatus: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
},
_isToday: true
};
setNextArrival(enriched);
} else {
setNextArrival(null);
}
setLoading(false);
return;
}
if (!supabase) return;
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([ const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
fetchAllRows(supabase.from('horaires').select('*')), fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'), supabase.from('travaux_substitutions').select('*'),
@@ -615,7 +654,7 @@ const AFLArriveesQuais = () => {
console.error(err); console.error(err);
setLoading(false); setLoading(false);
} }
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -706,7 +745,7 @@ const AFLArriveesQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) { if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
} }
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim(); const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1';
// Extraction des origines (si multi-origines / tranches) // Extraction des origines (si multi-origines / tranches)
let origins = []; let origins = [];
+87 -17
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -165,7 +178,15 @@ export default function AFLDeparts(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent // Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -195,9 +216,7 @@ export default function AFLDeparts(){
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -322,11 +341,14 @@ export default function AFLDeparts(){
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
}, []); }, []);
@@ -638,18 +660,54 @@ export default function AFLDeparts(){
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; // empêche les fetchs concurrents let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted) return; if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé if (inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement'); setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false); setLoading(false);
@@ -1049,7 +1107,7 @@ export default function AFLDeparts(){
const intervalId = setInterval(fetchData, intervalMs); const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]); }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
// Helpers pour perturbations (memoized plus haut) // Helpers pour perturbations (memoized plus haut)
@@ -1194,10 +1252,18 @@ export default function AFLDeparts(){
// Calcul des vias // Calcul des vias
const stopsList = getStops(horaire); const stopsList = getStops(horaire);
const viaNames = []; const viaNames = [];
if (Array.isArray(stopsList)) { if (Array.isArray(stopsList) && stopsList.length > 0) {
let isAfterStart = false; let isAfterStart = false;
const hasStartInStops = stopsList.some(s => {
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
return normalizeName(sName) === normalizeName(stationName || '');
});
if (!hasStartInStops) {
isAfterStart = true;
}
for (const s of stopsList) { for (const s of stopsList) {
const sName = s.gare_nom || s.nom || s.name || s.station_name || ''; const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
if (!sName) continue; if (!sName) continue;
const norm = normalizeName(sName); const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) { if (norm === normalizeName(stationName || '')) {
@@ -1336,6 +1402,10 @@ export default function AFLDeparts(){
<div className="afl-voie-box-compact"> <div className="afl-voie-box-compact">
<BusIcon size="52px" color="#002d62" /> <BusIcon size="52px" color="#002d62" />
</div> </div>
) : showPlatform ? (
<div className="afl-voie-box-compact">
<div className="afl-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null ) : null
)} )}
</div> </div>
+49 -10
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLDepartsQuais.css'; import './AFLDepartsQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo'; import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57) // Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`; return `${hh}:${mm}`;
} }
const parts = String(v).split(':'); const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (parts.length >= 2) { if (m) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`; const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
} }
return v; return v;
}; };
@@ -185,9 +188,12 @@ const AFLDepartsQuais = () => {
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/); const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
@@ -421,6 +427,39 @@ const AFLDepartsQuais = () => {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
const { data: sncfData } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs',
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const enriched = {
...train,
_rawTime: train.heure_depart,
_displayTime: formatTimeNoSeconds(train.heure_depart),
_status: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null
},
_nextStationStatus: {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
},
_isToday: true
};
setNextDeparture(enriched);
} else {
setNextDeparture(null);
}
setLoading(false);
return;
}
if (!supabase) return;
const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([ const [{ data: horairesData }, { data: subData }, { data: perturbData }, { data: travauxData }] = await Promise.all([
fetchAllRows(supabase.from('horaires').select('*')), fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'), supabase.from('travaux_substitutions').select('*'),
@@ -689,7 +728,7 @@ const AFLDepartsQuais = () => {
console.error(err); console.error(err);
setLoading(false); setLoading(false);
} }
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]); }, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -780,7 +819,7 @@ const AFLDepartsQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) { if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
} }
const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || quaiParam || '').toString().trim(); const platformStr = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || trainData.quai_reel || trainData.voie || trainData.quai || trainData._displayPlatform || quaiParam || '').toString().trim() || quaiParam || '1';
// Extraction des destinations (prise en compte des trains multi-branches / tranches) // Extraction des destinations (prise en compte des trains multi-branches / tranches)
let destinations = []; let destinations = [];
@@ -839,7 +878,7 @@ const AFLDepartsQuais = () => {
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName)); return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
}); });
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList.slice(1); const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
const seen = new Set(); const seen = new Set();
rawDownstream.forEach(s => { rawDownstream.forEach(s => {
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesArrivees(){
const [allHoraires, setAllHoraires] = useState([]); const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]); const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...) // type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null); const [, setStationType] = useState(null);
const [, setTravaux] = useState([]); const [, setTravaux] = useState([]);
const [, setLoading] = useState(false); const [, setLoading] = useState(false);
const [, setError] = useState(null); const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesArrivees(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent // Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesArrivees(){
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesArrivees(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant) // parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
return null; return null;
}, []); }, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => { const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return ''; if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,57 @@ export default function ClassiquesArrivees(){
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, []); }, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; // empêche les fetchs concurrents let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted) return; if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé if (inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement'); setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false); setLoading(false);
@@ -1013,7 +1060,7 @@ export default function ClassiquesArrivees(){
const intervalId = setInterval(fetchData, intervalMs); const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut) // Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1136,6 @@ export default function ClassiquesArrivees(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform); let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._arrHM === 'number') ? horaire._arrHM : parseTimeHM(horaire.heure_arrivee || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire); const isSub = isSubstitution(horaire);
const infoText = isSub const infoText = isSub
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesDeparts(){
const [allHoraires, setAllHoraires] = useState([]); const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]); const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...) // type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null); const [, setStationType] = useState(null);
const [, setTravaux] = useState([]); const [, setTravaux] = useState([]);
const [, setLoading] = useState(false); const [, setLoading] = useState(false);
const [, setError] = useState(null); const [, setError] = useState(null);
@@ -107,7 +120,15 @@ export default function ClassiquesDeparts(){
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent // Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesDeparts(){
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesDeparts(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant) // parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
}, []); }, []);
// retourne le threshold (en minutes) pour afficher le quai selon le type de gare
const thresholdForStationType = useCallback((typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
// Vérification du type de gare
if (tG === 'interurbaine' || tG.includes('interurb')) {
return 360; // 6 heures
}
if (tG === 'ville' || tG.includes('ville')) {
return 30; // 30 minutes
}
// par défaut, 30 minutes
return 30;
}, []);
const formatTimeFromHM = useCallback((totalMinutes) => { const formatTimeFromHM = useCallback((totalMinutes) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return ''; if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,54 @@ export default function ClassiquesDeparts(){
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, []); }, []);
// Récupération Supabase — maintenant avec polling pour synchroniser sans recharger la page // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; // empêche les fetchs concurrents let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted) return; if (!mounted) return;
if (inFlight) return; // éviter les appels simultanés si le fetch précédent n'est pas terminé if (inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeNoSeconds(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, defilementMode ? 20 : 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
// Supabase non initialisé — avertir et définir un message d'erreur pour l'UI
// eslint-disable-next-line no-console
console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY'); console.warn('Supabase non initialisé — vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY');
setError('Supabase non initialisé — vérifiez les variables d\'environnement'); setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false); setLoading(false);
@@ -1013,7 +1057,7 @@ export default function ClassiquesDeparts(){
const intervalId = setInterval(fetchData, intervalMs); const intervalId = setInterval(fetchData, intervalMs);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]); }, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
// Helpers pour perturbations (memoized plus haut) // Helpers pour perturbations (memoized plus haut)
@@ -1089,21 +1133,6 @@ export default function ClassiquesDeparts(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); const isDelayed = status && status.label && String(status.label).startsWith('retardé');
let showPlatform = !isSuppressed && Boolean(platform); let showPlatform = !isSuppressed && Boolean(platform);
if (showPlatform) {
const nowHM = currentTime.getHours()*60 + currentTime.getMinutes();
const sched = (typeof horaire._depHM === 'number') ? horaire._depHM : parseTimeHM(horaire.heure_depart || horaire.time || horaire.heure || horaire._rawTime || '');
const threshold = thresholdForStationType(stationType);
if (typeof sched === 'number') {
let diff = sched - nowHM;
// Gérer le passage à minuit
if (diff < -720) { // e.g., now 23:00, sched 01:00, diff is ~ -22 hours
diff += 1440; // treat as next day
}
showPlatform = (diff >= 0 && diff <= threshold);
} else {
showPlatform = false;
}
}
const isSub = isSubstitution(horaire); const isSub = isSubstitution(horaire);
const infoText = isSub const infoText = isSub
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() { function Clock() {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
useEffect(() => { useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesArriveesQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false); const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null); const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0); const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null); const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesArriveesQuais() {
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
}, []); }, []);
@@ -123,15 +128,26 @@ export default function ClassiquesArriveesQuais() {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0'); const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0'); const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesArriveesQuais() {
return `${hh}h${mm}`; return `${hh}h${mm}`;
}, []); }, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesArriveesQuais() {
setLoadingHoraire(true); setLoadingHoraire(true);
setHoraireError(null); setHoraireError(null);
setSelectedHoraire(null); setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'arrivees',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const arrHM = parseTimeHM(train.heure_arrivee || train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_arrivee_nom || gareDisplayParam || gareParam,
heure_arrivee: train.heure_arrivee || train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
arrHM: arrHM != null ? arrHM : nowHM,
rawTime: train.heure_arrivee || train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) { if (!supabase) {
setHoraireError('Supabase non initialisé'); setHoraireError('Supabase non initialisé');
setLoadingHoraire(false); setLoadingHoraire(false);
@@ -650,16 +712,10 @@ export default function ClassiquesArriveesQuais() {
loadNextForQuai(); loadNextForQuai();
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment // Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => { const showPlatform = true;
const sched = selectedHoraire ? selectedHoraire.arrHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
if (!loadingHoraire && !selectedHoraire) { if (!loadingHoraire && !selectedHoraire) {
return ( return (
@@ -789,7 +845,7 @@ export default function ClassiquesArriveesQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ } try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
} }
} }
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || []; if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => { const labeled = stops.map((stop, idx) => {
let label = ''; let label = '';
@@ -870,7 +926,7 @@ export default function ClassiquesArriveesQuais() {
{/* BOTTOM FOOTER: Full Width */} {/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-arrivees-redesign__footer"> <div className="aff-quais-arrivees-redesign__footer">
<div className="aff-quais-arrivees-redesign__footer-left"> <div className="aff-quais-arrivees-redesign__footer-left">
ain. {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div> </div>
<div className="aff-quais-arrivees-redesign__footer-right"> <div className="aff-quais-arrivees-redesign__footer-right">
<Clock /> <Clock />
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() { function Clock() {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
useEffect(() => { useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesDepartsQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false); const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null); const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0); const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null); const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null); const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,11 +113,14 @@ export default function ClassiquesDepartsQuais() {
const parseTimeHM = useCallback((s) => { const parseTimeHM = useCallback((s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
}, []); }, []);
@@ -123,15 +128,26 @@ export default function ClassiquesDepartsQuais() {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2,'0'); const hh = String(tryDate.getHours() % 24).padStart(2,'0');
const mm = String(tryDate.getMinutes()).padStart(2,'0'); const mm = String(tryDate.getMinutes()).padStart(2,'0');
return `${hh}h${mm}`; return `${hh}h${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2,'0')}h${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}h${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2,'0')}h${m3[2]}`;
}
return v.slice(0,5).replace(':','h'); return v.slice(0,5).replace(':','h');
} }
return String(v).slice(0,5).replace(':','h'); return String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesDepartsQuais() {
return `${hh}h${mm}`; return `${hh}h${mm}`;
}, []); }, []);
const thresholdForStationType = useCallback((type) => {
if (!type) return 0;
const t = String(type).trim().toLowerCase();
if (t === 'interurbaine' || t.includes('interurb')) return 360;
if (t === 'ville' || t.includes('ville')) return 30;
return 0;
}, []);
@@ -457,6 +467,58 @@ export default function ClassiquesDepartsQuais() {
setLoadingHoraire(true); setLoadingHoraire(true);
setHoraireError(null); setHoraireError(null);
setSelectedHoraire(null); setSelectedHoraire(null);
const source = getSourceFromPath(window.location.pathname);
if (source === 'sncf') {
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplayParam || gareParam,
mode: 'departs',
quai: quaiParam
});
if (!mounted) return;
if (sncfErr) {
setHoraireError(sncfErr);
} else if (sncfData && sncfData.length > 0) {
const train = sncfData[0];
const now = new Date();
const nowHM = now.getHours() * 60 + now.getMinutes();
const depHM = parseTimeHM(train.heure_depart);
const chosen = {
horaire: {
...train,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: train.retard_minutes || null,
causeDetails: train.cause_retard || null
}
},
stop: {
gare_nom: train.gare_depart_nom || gareDisplayParam || gareParam,
heure_depart: train.heure_depart,
quai: train.quai || quaiParam,
quai_reel: train.quai || quaiParam
},
depHM: depHM != null ? depHM : nowHM,
rawTime: train.heure_depart,
circulatesToday: true,
circulatesTomorrow: false,
isTomorrow: false
};
setSelectedHoraire(chosen);
setCandidatesCount(sncfData.length);
setStationType('interurbaine');
}
} catch (e) {
if (mounted) setHoraireError(e.message || String(e));
} finally {
if (mounted) setLoadingHoraire(false);
}
return;
}
if (!supabase) { if (!supabase) {
setHoraireError('Supabase non initialisé'); setHoraireError('Supabase non initialisé');
setLoadingHoraire(false); setLoadingHoraire(false);
@@ -648,16 +710,10 @@ export default function ClassiquesDepartsQuais() {
loadNextForQuai(); loadNextForQuai();
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareDisplayParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]); }, [gareDisplayParam, gareParam, gareIdParam, quaiParam, parseTimeHM, normalizeName, circulatesOn]);
// Rail is handled purely in CSS for better performance and alignment // Rail is handled purely in CSS for better performance and alignment
const showPlatform = (() => { const showPlatform = true;
const sched = selectedHoraire ? selectedHoraire.depHM : null;
if (sched === null || !stationType) return true;
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
const threshold = thresholdForStationType(stationType);
return nowHM >= (sched - threshold);
})();
if (!loadingHoraire && !selectedHoraire) { if (!loadingHoraire && !selectedHoraire) {
return ( return (
@@ -786,7 +842,7 @@ export default function ClassiquesDepartsQuais() {
try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ } try { const p = JSON.parse(horaire.gares_desservies); if (Array.isArray(p)) stops = p; } catch (e) { /* ignore */ }
} }
} }
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || []; if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => { const labeled = stops.map((stop, idx) => {
let label = ''; let label = '';
@@ -868,7 +924,7 @@ export default function ClassiquesDepartsQuais() {
{/* BOTTOM FOOTER: Full Width */} {/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-departs-redesign__footer"> <div className="aff-quais-departs-redesign__footer">
<div className="aff-quais-departs-redesign__footer-left"> <div className="aff-quais-departs-redesign__footer-left">
ain. {quaiParam ? `Voie ${quaiParam}` : (selectedHoraire?.horaire?.quai ? `Voie ${selectedHoraire.horaire.quai}` : (selectedHoraire?.stop?.quai ? `Voie ${selectedHoraire.stop.quai}` : ''))}
</div> </div>
<div className="aff-quais-departs-redesign__footer-right"> <div className="aff-quais-departs-redesign__footer-right">
<Clock /> <Clock />
+74 -11
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM avec deux points (ex: 22:39) // Helper utilitaire : format HH:MM avec deux points (ex: 22:39)
const formatTimeWithColon = (v) => { const formatTimeWithColon = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`; return `${hh}:${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}:${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m3[2]}`;
}
return v.slice(0, 5).replace('h', ':'); return v.slice(0, 5).replace('h', ':');
} }
return String(v).slice(0, 5).replace('h', ':'); return String(v).slice(0, 5).replace('h', ':');
@@ -445,7 +458,15 @@ export default function EVAArrivees() {
// Récupération type de gare // Récupération type de gare
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -475,9 +496,7 @@ export default function EVAArrivees() {
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -831,14 +850,55 @@ export default function EVAArrivees() {
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase avec polling // Récupération des données (SNCF GTFS ou Supabase) avec polling
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted || inFlight) return; if (!mounted || inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'arrivees'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
return {
...t,
_displayTime: formatTimeWithColon(timeStr),
_displayPlatform: t.quai || t.voie || '',
_arrHM: t._arrHM || 0,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
};
});
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
setLoading(false); setLoading(false);
inFlight = false; inFlight = false;
@@ -936,9 +996,12 @@ export default function EVAArrivees() {
const parseTimeHM = (s) => { const parseTimeHM = (s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
@@ -1129,7 +1192,7 @@ export default function EVAArrivees() {
fetchData(); fetchData();
const intervalId = setInterval(fetchData, 15000); const intervalId = setInterval(fetchData, 15000);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); }, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
// Alternance retard / texte toutes les 2s // Alternance retard / texte toutes les 2s
const [alternateDelay, setAlternateDelay] = useState(false); const [alternateDelay, setAlternateDelay] = useState(false);
+71 -11
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText'; import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HH:MM avec deux points (ex: 22:39) // Helper utilitaire : format HH:MM avec deux points (ex: 22:39)
const formatTimeWithColon = (v) => { const formatTimeWithColon = (v) => {
if (v == null) return ''; if (v == null) return '';
const tryDate = new Date(v); const tryDate = new Date(v);
if (!isNaN(tryDate.getTime())) { if (!isNaN(tryDate.getTime())) {
const hh = String(tryDate.getHours()).padStart(2, '0'); const hh = String(tryDate.getHours() % 24).padStart(2, '0');
const mm = String(tryDate.getMinutes()).padStart(2, '0'); const mm = String(tryDate.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`; return `${hh}:${mm}`;
} }
if (typeof v === 'string') { if (typeof v === 'string') {
const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/); const m = v.match(/(\d{1,2}):(\d{2})(?::\d{2})?/);
if (m) return `${String(m[1]).padStart(2, '0')}:${m[2]}`; if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
const m2 = v.match(/T(\d{2}):(\d{2})/); const m2 = v.match(/T(\d{2}):(\d{2})/);
if (m2) return `${m2[1]}:${m2[2]}`; if (m2) {
const h = parseInt(m2[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m2[2]}`;
}
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
if (m3) {
const h = parseInt(m3[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m3[2]}`;
}
return v.slice(0, 5).replace('h', ':'); return v.slice(0, 5).replace('h', ':');
} }
return String(v).slice(0, 5).replace('h', ':'); return String(v).slice(0, 5).replace('h', ':');
@@ -441,7 +454,15 @@ export default function EVADeparts() {
// Récupération type de gare // Récupération type de gare
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => { const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try { try {
let fetchedType = null; let fetchedType = null;
let fetchedId = gareIdParam || null; let fetchedId = gareIdParam || null;
@@ -471,9 +492,7 @@ export default function EVADeparts() {
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}; };
if (supabase) { fetchStationType();
fetchStationType();
}
return () => { mounted = false; }; return () => { mounted = false; };
}, [gareIdParam, gareDisplay]); }, [gareIdParam, gareDisplay]);
@@ -827,14 +846,52 @@ export default function EVADeparts() {
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
// Récupération Supabase avec polling // Récupération des données (SNCF GTFS ou Supabase) avec polling
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
let inFlight = false; let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => { const fetchData = async () => {
if (!mounted || inFlight) return; if (!mounted || inFlight) return;
inFlight = true; inFlight = true;
if (source === 'sncf') {
setLoading(true);
setError(null);
try {
const { data: sncfData, error: sncfErr } = await getSncfTimetable({
gareName: gareDisplay || gareParam,
mode: 'departs'
});
if (!mounted) { inFlight = false; return; }
if (sncfErr) {
setError(sncfErr);
} else {
const formatted = (sncfData || []).map(t => ({
...t,
_displayTime: formatTimeWithColon(t.heure_depart),
_displayPlatform: t.quai || t.voie || '',
_depHM: t._depHM || 0,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
delayMinutes: t.retard_minutes || null,
causeDetails: t.cause_retard || null
}
}));
setHoraires(formatted.slice(0, 10));
setAllHoraires(formatted);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
} finally {
if (mounted) setLoading(false);
inFlight = false;
}
return;
}
if (!supabase) { if (!supabase) {
setLoading(false); setLoading(false);
inFlight = false; inFlight = false;
@@ -952,9 +1009,12 @@ export default function EVADeparts() {
const parseTimeHM = (s) => { const parseTimeHM = (s) => {
if (!s) return null; if (!s) return null;
if (typeof s === 'number') return s; if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2}):(\d{2})/); const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10); if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s); const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes(); if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null; return null;
@@ -1145,7 +1205,7 @@ export default function EVADeparts() {
fetchData(); fetchData();
const intervalId = setInterval(fetchData, 15000); const intervalId = setInterval(fetchData, 15000);
return () => { mounted = false; clearInterval(intervalId); }; return () => { mounted = false; clearInterval(intervalId); };
}, [gareDisplay, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]); }, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
// Alternance retard / texte toutes les 2s // Alternance retard / texte toutes les 2s
const [alternateDelay, setAlternateDelay] = useState(false); const [alternateDelay, setAlternateDelay] = useState(false);