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
+1483 -477

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.\
You may also see any lint errors in the console.
## 📋 Patchnotes / Historique des Versions
### `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.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
<details open>
<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.\
It correctly bundles React in production mode and optimizes the build for the best performance.
</details>
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
<details>
<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
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)
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.
+347 -3
View File
@@ -16,6 +16,8 @@
"@testing-library/user-event": "^13.5.0",
"autoprefixer": "^10.4.27",
"eslint-plugin-react-hooks": "^7.0.1",
"gtfs-realtime-bindings": "^2.2.0",
"protobufjs": "^8.8.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^6.30.3",
@@ -2872,6 +2874,18 @@
"@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": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -3778,6 +3792,28 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"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": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -5606,6 +5642,18 @@
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"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": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -8872,6 +8920,18 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -11079,12 +11139,71 @@
"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": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"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": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -11283,6 +11402,15 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -11366,6 +11494,25 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"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": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
@@ -11406,9 +11553,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
@@ -11447,6 +11594,12 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11528,6 +11681,73 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -11543,6 +11763,12 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"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": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -13818,6 +14044,85 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"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": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -13861,6 +14166,15 @@
"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": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -14527,6 +14841,15 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"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": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -16330,6 +16653,15 @@
"@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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -16626,6 +16958,12 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -17772,6 +18110,12 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"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": {
"version": "4.38.3",
"resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz",
+2
View File
@@ -11,6 +11,8 @@
"autoprefixer": "^10.4.27",
"dependencies": "^0.0.1",
"eslint-plugin-react-hooks": "^7.0.1",
"gtfs-realtime-bindings": "^2.2.0",
"protobufjs": "^8.8.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^6.30.3",
+91 -62
View File
@@ -8,8 +8,7 @@ import Displays from './pages/Displays';
import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts';
import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees';
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 AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
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 SelectAfficheur from './pages/SelectAfficheur';
import AfficheurViewportWrapper from './components/AfficheurViewportWrapper';
import sessionMemory from './lib/sessionMemory';
import { getSourceFromPath } from './services/railwayService';
applyPolyfills().then(() => defineCustomElements(window));
// Petit routeur client sans dépendances externes
function useSimpleRouter() {
function normalizePath(p) {
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];
// traiter les cas comme /index.html
if (p.indexOf('index.html') !== -1) return '/';
// supprimer espaces en début/fin
p = p.trim();
// supprimer slashs finaux sauf pour la racine
if (p !== '/') p = p.replace(/\/+$/, '');
return p || '/';
}
@@ -49,24 +48,16 @@ function useSimpleRouter() {
function navigate(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 || '');
if (currentFull !== path) {
console.debug('[router] navigate ->', path, '(normalized ->', target, ')');
try {
window.history.pushState({}, '', path);
} catch (e) {
// fallback si path est invalide
try { window.history.pushState({}, '', target); } catch (err) { /* ignore */ }
try { window.history.pushState({}, '', target); } catch (err) {}
}
// mettre à jour la route interne (sans query/hash)
setRoute(target);
} else {
// si l'URL complète est identique, forcer un update de state pour
// déclencher un rendu si besoin (rare)
setRoute(target);
}
}
@@ -77,123 +68,161 @@ function useSimpleRouter() {
function App() {
const { route, navigate } = useSimpleRouter();
const source = getSourceFromPath(route);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const cleanRoute = (route.replace(/^\/ferrovia/, '') || '/');
// Composant Home explicite (panels cliquables)
function Home({ navigate }) {
useEffect(() => {
console.debug('[Home] monté, pathname =', window.location.pathname);
}, []);
function Home({ navigate, prefix, source }) {
const panelStyle = {
width: '40%',
width: '42%',
cursor: 'pointer',
padding: 24,
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
borderRadius: 10,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
background: '#fff',
textAlign: 'center'
textAlign: 'center',
border: '1px solid #e0e8f6',
transition: 'transform 0.15s ease'
};
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '40px', alignItems: 'center', background: '#e9f0ff', paddingTop: 32, paddingBottom: 32 }}>
<div style={{ width: '100%', textAlign: 'center', marginBottom: 16 }}>
<h1 style={{ margin: 0, fontSize: 28 }}>Accueil</h1>
<p style={{ margin: '8px 0 20px', color: '#333' }}>Choisissez une section ci-dessous</p>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '32px 24px', alignItems: 'center', background: '#e9f0ff', borderRadius: '12px' }}>
<div style={{ width: '100%', textAlign: 'center', marginBottom: 20 }}>
<div style={{ display: 'inline-block', padding: '4px 12px', borderRadius: '20px', background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe', color: source === 'supabase' ? '#137333' : '#1a73e8', fontWeight: 600, fontSize: '13px', marginBottom: '8px' }}>
{source === 'supabase' ? 'Mode Base de Données (Ferrovia)' : 'Mode Officiel SNCF (GTFS / Temps Réel)'}
</div>
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%' }}>
<h1 style={{ margin: '8px 0', fontSize: 28, color: '#002257' }}>Accueil</h1>
<p style={{ margin: '4px 0 16px', color: '#555' }}>
{source === 'supabase'
? "Consultez les afficheurs basés sur les données de votre BDD Supabase."
: "Consultez les afficheurs connectés aux flux réels GTFS et Open Data SNCF."}
</p>
</div>
<div style={{ display: 'flex', justifyContent: 'space-around', width: '100%', maxWidth: '800px' }}>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate('/departs'); })() : null)}
onClick={() => { try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {} navigate(`${prefix}/departs`); })() : null)}
style={panelStyle}
aria-label="Accéder aux départs"
>
<h2>Départs</h2>
<p>Consultez les prochains départs d'une gare. Cliquez pour choisir une gare et voir les horaires.</p>
<h2 style={{ color: '#00439e', marginTop: 0 }}>🚆 Départs</h2>
<p style={{ color: '#666', fontSize: '14px' }}>Consultez les prochains départs d'une gare. Choisissez une gare et le style d'afficheur.</p>
</div>
<div
role="button"
tabIndex={0}
onClick={() => { try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); }}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ' ? (function(){ try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {} navigate('/arrivees'); })() : null)}
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>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 }}>🏁 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>
);
}
// Routes pour afficheurs : format /afficheurs/{mode}/{type}?gare=...
// route plus spécifique pour l'afficheur "quais"
if (route.startsWith('/afficheurs/departs/classiques/quais')) {
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesDepartsQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/classiques/quais')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/classiques/quais')) {
return <AfficheurViewportWrapper><ClassiquesArriveesQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/classiques')) {
if (cleanRoute.startsWith('/afficheurs/departs/classiques')) {
return <AfficheurViewportWrapper><ClassiquesDeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/afl/quais')) {
if (cleanRoute.startsWith('/afficheurs/departs/afl/quais')) {
return <AfficheurViewportWrapper><AFLDepartsQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/afl')) {
if (cleanRoute.startsWith('/afficheurs/departs/afl')) {
return <AfficheurViewportWrapper><AFLDeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/departs/eva')) {
if (cleanRoute.startsWith('/afficheurs/departs/eva')) {
return <AfficheurViewportWrapper><EVADeparts /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/afl/quais')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/afl/quais')) {
return <AfficheurViewportWrapper><AFLArriveesQuais /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/afl')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/afl')) {
return <AfficheurViewportWrapper><AFLArrivees /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/eva')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/eva')) {
return <AfficheurViewportWrapper><EVAArrivees /></AfficheurViewportWrapper>;
}
if (route.startsWith('/afficheurs/arrivees/classiques')) {
if (cleanRoute.startsWith('/afficheurs/arrivees/classiques')) {
return <AfficheurViewportWrapper><ClassiquesArrivees /></AfficheurViewportWrapper>;
}
let content;
if (route === '/departs') {
content = <Departs onBack={() => navigate('/')} navigate={navigate} />;
} else if (route === '/arrivees') {
content = <Arrivees onBack={() => navigate('/')} navigate={navigate} />;
} else if (route === '/select-afficheur') {
if (cleanRoute === '/departs') {
content = <Departs onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (cleanRoute === '/arrivees') {
content = <Arrivees onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else if (cleanRoute === '/select-afficheur') {
content = <SelectAfficheur onBack={() => {
const params = new URLSearchParams(window.location.search);
const gare = params.get('gare') || '';
const mode = params.get('mode') || 'departs';
navigate(`/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(gare)}?mode=${mode}`);
}} navigate={navigate} />;
} else if (route.startsWith('/afficheurs')) {
content = <Displays onBack={() => navigate('/')} navigate={navigate} />;
} else if (cleanRoute.startsWith('/afficheurs')) {
content = <Displays onBack={() => navigate(prefix || '/')} navigate={navigate} />;
} else {
content = <Home navigate={navigate} />;
content = <Home navigate={navigate} prefix={prefix} source={source} />;
}
return (
<div className="sncf-voyageurs" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<wcs-galactic app-name="Affichage Ferroviaire" version="1.0.0">
<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>
<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>
<header style={{ padding: 12, background: '#00439e', color: '#fff' }}>
<div style={{ maxWidth: 1000, margin: '0 auto' }}>
<h1 style={{ margin: 0, fontSize: 20 }}>Affichage Ferroviaire — Accueil</h1>
<header style={{ padding: 12, background: source === 'supabase' ? '#003366' : '#00439e', color: '#fff', transition: 'background 0.3s' }}>
<div style={{ maxWidth: 1000, margin: '0 auto', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<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>
</header>
<main style={{ flex: 1, background: '#f3f6fb' }}>
+110 -46
View File
@@ -1,33 +1,28 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import React, { useEffect, useState, useMemo } from 'react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Arrivees({ onBack, navigate }) {
const [gares, setGares] = useState([]);
const [loading, setLoading] = useState(true);
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const [allGares, setAllGares] = useState([]);
const [sncfResults, setSncfResults] = useState([]);
const [loading, setLoading] = useState(source === 'supabase');
const [error, setError] = useState(null);
const [selected, setSelected] = useState('');
const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => {
let mounted = true;
async function loadGares() {
if (source === 'supabase') {
async function loadSupabaseGares() {
setLoading(true);
setError(null);
if (!supabase) {
if (mounted) {
setError(
"Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)."
);
setLoading(false);
}
return;
}
try {
const { data, error } = await supabase.from('gares').select('nom');
if (error) throw error;
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []);
const data = await searchGares('', 'supabase');
if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
@@ -35,68 +30,139 @@ export default function Arrivees({ onBack, navigate }) {
if (mounted) setLoading(false);
}
}
loadSupabaseGares();
}
return () => { mounted = false; };
}, [source]);
loadGares();
// 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;
}
const timer = setTimeout(async () => {
try {
const results = await searchGares(q, 'sncf');
if (active) setSncfResults(results);
} catch (e) {
if (active) setSncfResults([]);
}
}, 200);
return () => {
mounted = false;
active = false;
clearTimeout(timer);
};
}
}, [search, source]);
// 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]);
const handleSelectGare = (nom) => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`);
}
};
}, []);
return (
<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>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && (
<div>
{/* Barre de recherche centrée */}
{/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input
type="search"
placeholder="Rechercher une gare..."
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare"
autoFocus
/>
</div>
{/* Résultats sous forme de lignes */}
<div role="list" aria-label="Résultats des gares">
{gares
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase()))
.map((nom) => (
<div key={nom} role="listitem">
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
<p style={{ margin: 0, fontSize: '15px' }}>
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
Aucune gare SNCF trouvée pour "{search}".
</div>
)}
{/* Résultats */}
{displayedGares.length > 0 && (
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div key={g.uic || nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }}
onClick={() => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`);
}}
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=arrivees`) : setSelected(nom)) : null)}
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>{nom}</div>
<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 style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} />
</div>
))}
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
</div>
);
})}
</div>
)}
{selected && (
<div style={{ marginTop: 12 }}>
<div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => {
try { sessionMemory.set('afficheurMode', 'arrivees'); } catch (e) {}
if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
} else {
alert(`Afficher les arrivées pour ${selected}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=arrivees`);
}
}}>Voir les arrivées</button>
</div>
@@ -110,5 +176,3 @@ export default function Arrivees({ onBack, navigate }) {
</div>
);
}
+110 -47
View File
@@ -1,34 +1,28 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import React, { useEffect, useState, useMemo } from 'react';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath, searchGares } from '../services/railwayService';
export default function Departs({ onBack, navigate }) {
const [gares, setGares] = useState([]);
const [loading, setLoading] = useState(true);
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const [allGares, setAllGares] = useState([]);
const [sncfResults, setSncfResults] = useState([]);
const [loading, setLoading] = useState(source === 'supabase');
const [error, setError] = useState(null);
const [selected, setSelected] = useState('');
const [search, setSearch] = useState('');
// Pour Supabase : charger les gares une seule fois au montage
useEffect(() => {
let mounted = true;
async function loadGares() {
if (source === 'supabase') {
async function loadSupabaseGares() {
setLoading(true);
setError(null);
if (!supabase) {
// Le client Supabase n'a pas pu être initialisé (variables d'env manquantes)
if (mounted) {
setError(
"Supabase non initialisé : vérifiez REACT_APP_SUPABASE_URL et REACT_APP_SUPABASE_ANON_KEY dans votre .env et redémarrez le serveur de développement (npm start)."
);
setLoading(false);
}
return;
}
try {
const { data, error } = await supabase.from('gares').select('nom');
if (error) throw error;
if (mounted) setGares(Array.isArray(data) ? data.map((r) => r.nom) : []);
const data = await searchGares('', 'supabase');
if (mounted) setAllGares(Array.isArray(data) ? data.map(r => r.nom || r) : []);
} catch (err) {
console.error('Supabase error:', err);
if (mounted) setError(err.message || String(err));
@@ -36,68 +30,139 @@ export default function Departs({ onBack, navigate }) {
if (mounted) setLoading(false);
}
}
loadSupabaseGares();
}
return () => { mounted = false; };
}, [source]);
loadGares();
// 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;
}
const timer = setTimeout(async () => {
try {
const results = await searchGares(q, 'sncf');
if (active) setSncfResults(results);
} catch (e) {
if (active) setSncfResults([]);
}
}, 200);
return () => {
mounted = false;
active = false;
clearTimeout(timer);
};
}
}, [search, source]);
// 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]);
const handleSelectGare = (nom) => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) {
navigate(`${prefix}/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}
};
}, []);
return (
<div style={{ padding: 20 }}>
<h1>Départs</h1>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h1 style={{ margin: 0 }}>Départs</h1>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
{loading && <p>Chargement des gares...</p>}
{error && <p style={{ color: 'red' }}>Erreur : {error}</p>}
{!loading && !error && (
<div>
{/* Barre de recherche centrée */}
{/* Barre de recherche */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }}>
<input
type="search"
placeholder="Rechercher une gare..."
placeholder={source === 'sncf' ? "Rechercher une gare SNCF (ex: Dijon, Paris, Lyon...)" : "Rechercher une gare..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '60%', padding: '10px 12px', borderRadius: 6, border: '1px solid #ccc' }}
aria-label="Rechercher une gare"
autoFocus
/>
</div>
{/* Résultats sous forme de lignes */}
<div role="list" aria-label="Résultats des gares">
{gares
.filter((nom) => nom.toLowerCase().includes(search.trim().toLowerCase()))
.map((nom) => (
<div key={nom} role="listitem">
{/* Invitation à la recherche en mode SNCF */}
{source === 'sncf' && search.trim().length < 2 && (
<div style={{ textAlign: 'center', padding: '30px 16px', color: '#666', background: '#fff', borderRadius: '8px', border: '1px dashed #ccc' }}>
<p style={{ margin: 0, fontSize: '15px' }}>
🔍 <strong>Saisissez au moins 2 caractères</strong> pour rechercher parmi les gares du référentiel GTFS SNCF.
</p>
</div>
)}
{/* Aucun résultat */}
{source === 'sncf' && search.trim().length >= 2 && displayedGares.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px', color: '#777' }}>
Aucune gare SNCF trouvée pour "{search}".
</div>
)}
{/* Résultats */}
{displayedGares.length > 0 && (
<div role="list" aria-label="Résultats des gares" style={{ background: '#fff', borderRadius: '8px', padding: '8px 16px', boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}>
{displayedGares.map((g) => {
const nom = g.nom;
return (
<div key={g.uic || nom} role="listitem">
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', cursor: 'pointer' }}
onClick={() => {
setSelected(nom);
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`);
}}
onKeyDown={(e) => (e.key === 'Enter' ? (navigate ? navigate(`/afficheurs/${encodeURIComponent(nom)}?mode=departs`) : setSelected(nom)) : null)}
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>{nom}</div>
<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 style={{ height: 1, background: '#e0e0e0', margin: '6px 0' }} />
</div>
))}
<div style={{ height: 1, background: '#eef1f6', margin: '2px 0' }} />
</div>
);
})}
</div>
)}
{selected && (
<div style={{ marginTop: 12 }}>
<div style={{ marginTop: 16 }}>
<p>Gare sélectionnée : <strong>{selected}</strong></p>
<button onClick={() => {
try { sessionMemory.set('afficheurMode', 'departs'); } catch (e) {}
if (navigate) {
navigate(`/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
} else {
alert(`Afficher les départs pour ${selected}`);
navigate(`${prefix}/afficheurs/${encodeURIComponent(selected)}?mode=departs`);
}
}}>Voir les départs</button>
</div>
@@ -111,5 +176,3 @@ export default function Departs({ onBack, navigate }) {
</div>
);
}
+35 -27
View File
@@ -1,8 +1,12 @@
import React from 'react';
import WcsButton from '../components/WcsButton';
import sessionMemory from '../lib/sessionMemory';
import { getSourceFromPath } from '../services/railwayService';
export default function Displays({ onBack, navigate }) {
const source = getSourceFromPath(window.location.pathname);
const prefix = source === 'supabase' ? '/ferrovia' : '';
const types = [
{ id: 'classiques', label: 'Classiques' },
{ id: 'eva', label: 'EVA' },
@@ -10,25 +14,23 @@ export default function Displays({ onBack, navigate }) {
{ id: 'transilien', label: 'TRANSILIEN' }
];
// Récupérer la gare : soit dans le pathname (/afficheurs/<gare>),
// soit via la querystring ?gare=... si la route est déjà /afficheurs/<type>/<mode>
const parts = typeof window !== 'undefined' ? window.location.pathname.split('/').filter(Boolean) : [];
// Nettoyage du chemin pour extraire la gare même avec /ferrovia
const cleanPath = (typeof window !== 'undefined' ? window.location.pathname : '').replace(/^\/ferrovia/, '');
const parts = cleanPath.split('/').filter(Boolean);
const typeIds = types.map(t => t.id);
let gare = null;
if (parts[0] === 'afficheurs' && parts[1] && !typeIds.includes(parts[1])) {
// cas /afficheurs/<gare>
try {
gare = decodeURIComponent(parts.slice(1).join('/'));
} catch (e) {
gare = parts.slice(1).join('/');
}
} else {
// si l'URL est /afficheurs/<type>/<mode>, regarder dans la querystring ?gare=...
const paramsForGare = new URLSearchParams(window.location.search || '');
const g = paramsForGare.get('gare');
if (g) {
try {
// décoder puis remplacer les tirets par des espaces pour l'affichage
gare = decodeURIComponent(String(g).replace(/\+/g, ' ')).replace(/-/g, ' ');
} catch (e) {
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 || '');
let mode = params.get('mode');
if (!mode) {
try {
const stored = sessionMemory.get('afficheurMode');
if (stored) mode = stored;
} catch (e) {
// ignore
}
} catch (e) {}
}
if (!mode) mode = 'departs';
return (
<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>}
<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 }}>
{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
key={t.id}
role="button"
tabIndex={0}
style={{ width: 220, padding: 16, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.08)', background: '#fff', cursor: 'pointer' }}
onClick={() => {
if (typeof navigate === 'function') {
const kebab = gare ? String(gare).trim().replace(/\s+/g, '-').replace(/\//g, '/') : '';
navigate(`/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
navigate(`${prefix}/select-afficheur?gare=${encodeURIComponent(kebab)}&mode=${mode}&type=${t.id}`);
return;
}
alert(`${t.label} pour ${gare || '—'}`);
}}>
}}
>
<h3 style={{ margin: '0 0 8px' }}>{t.label}</h3>
<p style={{ margin: 0, color: '#555' }}>Afficher le style {t.label} pour la gare {gare || ''}.</p>
</div>
@@ -74,29 +91,21 @@ export default function Displays({ onBack, navigate }) {
</div>
<div style={{ marginTop: 24 }}>
{/* Bouton de retour au style WCS SNCF */}
<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"
className="wcs-primary"
onClick={() => {
// Appelle le callback fourni par le routeur simple si disponible
try {
if (typeof onBack === 'function') {
onBack();
return;
}
} catch (e) {
// ignore
}
} catch (e) {}
// Fallback : si l'historique du navigateur permet un back, on l'utilise
if (typeof window !== 'undefined' && window.history && window.history.length > 1) {
window.history.back();
} else {
// Redirection sûre vers la racine
window.location.href = '/';
window.location.href = prefix || '/';
}
}}
label="Retour"
@@ -105,4 +114,3 @@ export default function Displays({ onBack, navigate }) {
</div>
);
}
+25 -47
View File
@@ -1,8 +1,11 @@
import React, { useEffect, useState } from 'react';
import supabase from '../lib/supabase';
import WcsButton from '../components/WcsButton';
import { getSourceFromPath, getPlatformsForStation } from '../services/railwayService';
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 gareParam = params.get('gare') || '';
const modeParam = params.get('mode') || 'departs';
@@ -21,50 +24,13 @@ export default function SelectAfficheur({ onBack, navigate }) {
useEffect(() => {
let mounted = true;
async function loadPlatforms() {
if (!supabase) {
setLoading(false);
return;
}
try {
setLoading(true);
const { data, error } = await supabase.from('horaires').select('quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom, gares_desservies');
if (error) throw error;
const sortedPlatforms = await getPlatformsForStation(decodedGare || gareParam, source);
if (!mounted) return;
const platformSet = new Set();
const normGare = decodedGare.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
(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) {
setPlatforms(sortedPlatforms || []);
if (sortedPlatforms && sortedPlatforms.length > 0) {
setSelectedPlatform(sortedPlatforms[0]);
}
} catch (err) {
@@ -78,12 +44,12 @@ export default function SelectAfficheur({ onBack, navigate }) {
return () => {
mounted = false;
};
}, [gareParam, decodedGare]);
}, [gareParam, decodedGare, source]);
const handleGeneral = () => {
if (typeof navigate === 'function') {
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') {
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)',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<h2 style={{
marginTop: 0,
marginBottom: '8px',
margin: 0,
color: '#002257',
fontSize: '28px',
fontWeight: 700
}}>
Sélection de l'Afficheur
</h2>
<span style={{
fontSize: '12px',
fontWeight: '600',
padding: '4px 10px',
borderRadius: '12px',
background: source === 'supabase' ? '#e6f4ea' : '#e8f0fe',
color: source === 'supabase' ? '#137333' : '#1a73e8'
}}>
{source === 'supabase' ? '🗄️ BDD Ferrovia' : '🌐 SNCF (GTFS)'}
</span>
</div>
<p style={{
margin: '0 0 24px 0',
color: '#555555',
@@ -268,7 +246,7 @@ export default function SelectAfficheur({ onBack, navigate }) {
if (typeof onBack === 'function') {
onBack();
} 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"
+78 -14
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0,5).replace(':','h');
@@ -155,10 +168,17 @@ export default function AFLArrivees(){
useAnnonceSonoreRealtime(resolvedGareId, 'AFLArrivees');
useAutoAnnonce(horaires, currentTime, true);
// Récupérer le type de gare une seule fois au montage ou quand les paramètres changent
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -188,9 +208,7 @@ export default function AFLArrivees(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -315,9 +333,12 @@ export default function AFLArrivees(){
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -626,18 +647,57 @@ export default function AFLArrivees(){
});
}, [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(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
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;
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) {
// 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');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1022,7 +1082,7 @@ export default function AFLArrivees(){
const intervalId = setInterval(fetchData, intervalMs);
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)
@@ -1301,6 +1361,10 @@ export default function AFLArrivees(){
<div className="afl-arr-voie-box-compact">
<BusIcon size="52px" color="#173e31" />
</div>
) : showPlatform ? (
<div className="afl-arr-voie-box-compact">
<div className="afl-arr-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null
)}
</div>
+48 -9
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLArriveesQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
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)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}:${mm}`;
}
const parts = String(v).split(':');
if (parts.length >= 2) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
return v;
};
@@ -137,9 +140,12 @@ const AFLArriveesQuais = () => {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -351,6 +357,39 @@ const AFLArriveesQuais = () => {
const fetchData = useCallback(async () => {
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([
fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'),
@@ -615,7 +654,7 @@ const AFLArriveesQuais = () => {
console.error(err);
setLoading(false);
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => {
fetchData();
@@ -706,7 +745,7 @@ const AFLArriveesQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
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)
let origins = [];
+85 -15
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 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
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -195,9 +216,7 @@ export default function AFLDeparts(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -322,9 +341,12 @@ export default function AFLDeparts(){
// eslint-disable-next-line no-unused-vars
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -638,18 +660,54 @@ export default function AFLDeparts(){
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(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
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;
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) {
// 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');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1049,7 +1107,7 @@ export default function AFLDeparts(){
const intervalId = setInterval(fetchData, intervalMs);
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)
@@ -1194,10 +1252,18 @@ export default function AFLDeparts(){
// Calcul des vias
const stopsList = getStops(horaire);
const viaNames = [];
if (Array.isArray(stopsList)) {
if (Array.isArray(stopsList) && stopsList.length > 0) {
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) {
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;
const norm = normalizeName(sName);
if (norm === normalizeName(stationName || '')) {
@@ -1336,6 +1402,10 @@ export default function AFLDeparts(){
<div className="afl-voie-box-compact">
<BusIcon size="52px" color="#002d62" />
</div>
) : showPlatform ? (
<div className="afl-voie-box-compact">
<div className="afl-voie-value" style={{ fontSize: '58px', lineHeight: 1 }}>{rawPlatform}</div>
</div>
) : null
)}
</div>
+49 -10
View File
@@ -4,19 +4,22 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import './AFLDepartsQuais.css';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
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)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}:${mm}`;
}
const parts = String(v).split(':');
if (parts.length >= 2) {
return `${parts[0].padStart(2, '0')}:${parts[1].padStart(2, '0')}`;
const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return `${String(h).padStart(2, '0')}:${m[2]}`;
}
return v;
};
@@ -185,9 +188,12 @@ const AFLDepartsQuais = () => {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[h:](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -421,6 +427,39 @@ const AFLDepartsQuais = () => {
const fetchData = useCallback(async () => {
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([
fetchAllRows(supabase.from('horaires').select('*')),
supabase.from('travaux_substitutions').select('*'),
@@ -689,7 +728,7 @@ const AFLDepartsQuais = () => {
console.error(err);
setLoading(false);
}
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
useEffect(() => {
fetchData();
@@ -780,7 +819,7 @@ const AFLDepartsQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
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)
let destinations = [];
@@ -839,7 +878,7 @@ const AFLDepartsQuais = () => {
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();
rawDownstream.forEach(s => {
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesArrivees(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
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
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesArrivees(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesArrivees(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours()*60 + d.getMinutes();
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) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,57 @@ export default function ClassiquesArrivees(){
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(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
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;
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) {
// 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');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1060,7 @@ export default function ClassiquesArrivees(){
const intervalId = setInterval(fetchData, intervalMs);
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)
@@ -1089,21 +1136,6 @@ export default function ClassiquesArrivees(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
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 infoText = isSub
@@ -6,21 +6,34 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0,5).replace(':','h');
@@ -93,7 +106,7 @@ export default function ClassiquesDeparts(){
const [allHoraires, setAllHoraires] = useState([]);
const [perturbations, setPerturbations] = useState([]);
// type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...)
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [, setTravaux] = useState([]);
const [, setLoading] = useState(false);
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
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('interurbaine');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -137,9 +158,7 @@ export default function ClassiquesDeparts(){
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -263,29 +282,18 @@ export default function ClassiquesDeparts(){
// parse HH:mm or ISO into minutes since midnight (utilisable ailleurs dans le composant)
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
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) => {
if (typeof totalMinutes !== 'number' || isNaN(totalMinutes)) return '';
@@ -604,18 +612,54 @@ export default function ClassiquesDeparts(){
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(() => {
let mounted = true;
let inFlight = false; // empêche les fetchs concurrents
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
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;
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) {
// 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');
setError('Supabase non initialisé — vérifiez les variables d\'environnement');
setLoading(false);
@@ -1013,7 +1057,7 @@ export default function ClassiquesDeparts(){
const intervalId = setInterval(fetchData, intervalMs);
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)
@@ -1089,21 +1133,6 @@ export default function ClassiquesDeparts(){
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
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 infoText = isSub
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesArriveesQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,9 +113,12 @@ export default function ClassiquesArriveesQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -123,15 +128,26 @@ export default function ClassiquesArriveesQuais() {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesArriveesQuais() {
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);
setHoraireError(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) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -650,16 +712,10 @@ export default function ClassiquesArriveesQuais() {
loadNextForQuai();
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
const showPlatform = (() => {
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);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
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 */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -870,7 +926,7 @@ export default function ClassiquesArriveesQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-arrivees-redesign__footer">
<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 className="aff-quais-arrivees-redesign__footer-right">
<Clock />
@@ -4,6 +4,8 @@ import supabase from '../../../../lib/supabase';
import { fetchAllRows } from '../../../../lib/fetchAllRows';
import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
@@ -46,7 +48,7 @@ export default function ClassiquesDepartsQuais() {
const [loadingHoraire, setLoadingHoraire] = useState(false);
const [, setHoraireError] = useState(null);
const [, setCandidatesCount] = useState(0);
const [stationType, setStationType] = useState(null);
const [, setStationType] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
@@ -111,9 +113,12 @@ export default function ClassiquesDepartsQuais() {
const parseTimeHM = useCallback((s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1],10)*60 + parseInt(m[2],10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -123,15 +128,26 @@ export default function ClassiquesDepartsQuais() {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}h${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0,5).replace(':','h');
@@ -145,13 +161,7 @@ export default function ClassiquesDepartsQuais() {
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);
setHoraireError(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) {
setHoraireError('Supabase non initialisé');
setLoadingHoraire(false);
@@ -648,16 +710,10 @@ export default function ClassiquesDepartsQuais() {
loadNextForQuai();
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
const showPlatform = (() => {
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);
})();
const showPlatform = true;
if (!loadingHoraire && !selectedHoraire) {
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 */ }
}
}
if (!Array.isArray(stops) || stops.length === 0) stops = mock.stops || [];
if (!Array.isArray(stops) || stops.length === 0) stops = [];
const labeled = stops.map((stop, idx) => {
let label = '';
@@ -868,7 +924,7 @@ export default function ClassiquesDepartsQuais() {
{/* BOTTOM FOOTER: Full Width */}
<div className="aff-quais-departs-redesign__footer">
<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 className="aff-quais-departs-redesign__footer-right">
<Clock />
+73 -10
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
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)
const formatTimeWithColon = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}:${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0, 5).replace('h', ':');
@@ -445,7 +458,15 @@ export default function EVAArrivees() {
// Récupération type de gare
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -475,9 +496,7 @@ export default function EVAArrivees() {
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -831,14 +850,55 @@ export default function EVAArrivees() {
});
}, [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(() => {
let mounted = true;
let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted || inFlight) return;
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) {
setLoading(false);
inFlight = false;
@@ -936,9 +996,12 @@ export default function EVAArrivees() {
const parseTimeHM = (s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -1129,7 +1192,7 @@ export default function EVAArrivees() {
fetchData();
const intervalId = setInterval(fetchData, 15000);
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
const [alternateDelay, setAlternateDelay] = useState(false);
+70 -10
View File
@@ -5,21 +5,34 @@ import { fetchAllRows } from '../../../lib/fetchAllRows';
import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
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)
const formatTimeWithColon = (v) => {
if (v == null) return '';
const tryDate = new Date(v);
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');
return `${hh}:${mm}`;
}
if (typeof v === 'string') {
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})/);
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 String(v).slice(0, 5).replace('h', ':');
@@ -441,7 +454,15 @@ export default function EVADeparts() {
// Récupération type de gare
useEffect(() => {
let mounted = true;
const source = getSourceFromPath(window.location.pathname);
const fetchStationType = async () => {
if (source === 'sncf') {
if (mounted) {
setStationType('ville');
setResolvedGareId(null);
}
return;
}
try {
let fetchedType = null;
let fetchedId = gareIdParam || null;
@@ -471,9 +492,7 @@ export default function EVADeparts() {
}
} catch (e) { /* ignore */ }
};
if (supabase) {
fetchStationType();
}
return () => { mounted = false; };
}, [gareIdParam, gareDisplay]);
@@ -827,14 +846,52 @@ export default function EVADeparts() {
});
}, [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(() => {
let mounted = true;
let inFlight = false;
const source = getSourceFromPath(window.location.pathname);
const fetchData = async () => {
if (!mounted || inFlight) return;
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) {
setLoading(false);
inFlight = false;
@@ -952,9 +1009,12 @@ export default function EVADeparts() {
const parseTimeHM = (s) => {
if (!s) return null;
if (typeof s === 'number') return s;
const m = String(s).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
if (typeof s === 'number') return s % 1440;
const m = String(s).match(/(\d{1,2})[:h](\d{2})/i);
if (m) {
const h = parseInt(m[1], 10) % 24;
return h * 60 + parseInt(m[2], 10);
}
const d = new Date(s);
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
return null;
@@ -1145,7 +1205,7 @@ export default function EVADeparts() {
fetchData();
const intervalId = setInterval(fetchData, 15000);
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
const [alternateDelay, setAlternateDelay] = useState(false);