Compare commits
10
Commits
8449f502a4
...
d7d4f9cecf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d4f9cecf | ||
|
|
d8f8ceaa9f | ||
|
|
9c6ac798f1 | ||
|
|
b90c699302 | ||
|
|
e351398fe6 | ||
|
|
5d954af44a | ||
|
|
75d0a05f21 | ||
|
|
a084f699ac | ||
|
|
4df19e55e7 | ||
|
|
647ec37d99 |
No files matched your search
@@ -1,2 +1,3 @@
|
||||
REACT_APP_SUPABASE_URL=https://supabase-ferrovia.mrpatator.fr
|
||||
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzg3NjY3MjMxLCJleHAiOjE5NDUzNDcyMzF9.uD7YUlOFIhK_Jqbra94o5-N5zdrzKezoCxV067RJi78
|
||||
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzkwNzY2MzE5LCJleHAiOjE5NDg0NDYzMTl9.N1znIiUGg8txVhd2Kbf019uo4AcM-3yfumRaZHR8ymE
|
||||
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_6dB95QNDKiHcFD81TJETqq_3Mu5rWWU
|
||||
+64
-64
@@ -3,379 +3,379 @@
|
||||
"logos": [
|
||||
{
|
||||
"file": "logo-aleop.svg",
|
||||
"path": "/img/type/logo-aleop.svg",
|
||||
"path": "/logo-aleop.svg",
|
||||
"slug": "aleop",
|
||||
"name": "Aleop"
|
||||
},
|
||||
{
|
||||
"file": "logo-auvergerhonealpes.svg",
|
||||
"path": "/img/type/logo-auvergerhonealpes.svg",
|
||||
"path": "/logo-auvergerhonealpes.svg",
|
||||
"slug": "auvergerhonealpes",
|
||||
"name": "Auvergerhonealpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-breizhgo.svg",
|
||||
"path": "/img/type/logo-breizhgo.svg",
|
||||
"path": "/logo-breizhgo.svg",,
|
||||
"slug": "breizhgo",
|
||||
"name": "Breizhgo"
|
||||
},
|
||||
{
|
||||
"file": "logo-cfl.svg",
|
||||
"path": "/img/type/logo-cfl.svg",
|
||||
"path": "/logo-cfl.svg",
|
||||
"slug": "cfl",
|
||||
"name": "CFL"
|
||||
},
|
||||
{
|
||||
"file": "logo-db.svg",
|
||||
"path": "/img/type/logo-db.svg",
|
||||
"path": "/logo-db.svg",
|
||||
"slug": "db",
|
||||
"name": "DB"
|
||||
},
|
||||
{
|
||||
"file": "logo-eurostar.svg",
|
||||
"path": "/img/type/logo-eurostar.svg",
|
||||
"path": "/logo-eurostar.svg",
|
||||
"slug": "eurostar",
|
||||
"name": "Eurostar"
|
||||
},
|
||||
{
|
||||
"file": "logo-fluo.svg",
|
||||
"path": "/img/type/logo-fluo.svg",
|
||||
"path": "/logo-fluo.svg",
|
||||
"slug": "fluo",
|
||||
"name": "Fluo"
|
||||
},
|
||||
{
|
||||
"file": "logo-frecciarossa.svg",
|
||||
"path": "/img/type/logo-frecciarossa.svg",
|
||||
"path": "/logo-frecciarossa.svg",
|
||||
"slug": "frecciarossa",
|
||||
"name": "Frecciarossa"
|
||||
},
|
||||
{
|
||||
"file": "logo-hautsdefrance.svg",
|
||||
"path": "/img/type/logo-hautsdefrance.svg",
|
||||
"path": "/logo-hautsdefrance.svg",
|
||||
"slug": "hautsdefrance",
|
||||
"name": "Hautsdefrance"
|
||||
},
|
||||
{
|
||||
"file": "logo-ice.svg",
|
||||
"path": "/img/type/logo-ice.svg",
|
||||
"path": "/logo-ice.svg",
|
||||
"slug": "ice",
|
||||
"name": "ICE"
|
||||
},
|
||||
{
|
||||
"file": "logo-intercites.svg",
|
||||
"path": "/img/type/logo-intercites.svg",
|
||||
"path": "/logo-intercites.svg",
|
||||
"slug": "intercites",
|
||||
"name": "Intercités"
|
||||
},
|
||||
{
|
||||
"file": "logo-ler.svg",
|
||||
"path": "/img/type/logo-ler.svg",
|
||||
"path": "/logo-ler.svg",
|
||||
"slug": "ler",
|
||||
"name": "Ler"
|
||||
},
|
||||
{
|
||||
"file": "logo-lio.svg",
|
||||
"path": "/img/type/logo-lio.svg",
|
||||
"path": "/logo-lio.svg",
|
||||
"slug": "lio",
|
||||
"name": "Lio"
|
||||
},
|
||||
{
|
||||
"file": "logo-lunea.svg",
|
||||
"path": "/img/type/logo-lunea.svg",
|
||||
"path": "/logo-lunea.svg",
|
||||
"slug": "lunea",
|
||||
"name": "Lunea"
|
||||
},
|
||||
{
|
||||
"file": "logo-metrolor.svg",
|
||||
"path": "/img/type/logo-metrolor.svg",
|
||||
"path": "/logo-metrolor.svg",
|
||||
"slug": "metrolor",
|
||||
"name": "Metrolor"
|
||||
},
|
||||
{
|
||||
"file": "logo-mobigo.svg",
|
||||
"path": "/img/type/logo-mobigo.svg",
|
||||
"path": "/logo-mobigo.svg",
|
||||
"slug": "mobigo",
|
||||
"name": "Mobigo"
|
||||
},
|
||||
{
|
||||
"file": "logo-nomad.svg",
|
||||
"path": "/img/type/logo-nomad.svg",
|
||||
"path": "/logo-nomad.svg",
|
||||
"slug": "nomad",
|
||||
"name": "Nomad"
|
||||
},
|
||||
{
|
||||
"file": "logo-occitanie.svg",
|
||||
"path": "/img/type/logo-occitanie.svg",
|
||||
"path": "/logo-occitanie.svg",
|
||||
"slug": "occitanie",
|
||||
"name": "Occitanie"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo.svg",
|
||||
"path": "/img/type/logo-ouigo.svg",
|
||||
"path": "/logo-ouigo.svg",
|
||||
"slug": "ouigo",
|
||||
"name": "TGV Ouigo"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-classique.svg",
|
||||
"path": "/img/type/logo-ouigo-classique.svg",
|
||||
"path": "/logo-ouigo-classique.svg",
|
||||
"slug": "ouigo-classique",
|
||||
"name": "OUIGO Trains Classiques"
|
||||
},
|
||||
{
|
||||
"file": "logo-remi.svg",
|
||||
"path": "/img/type/logo-remi.svg",
|
||||
"path": "/logo-remi.svg",
|
||||
"slug": "remi",
|
||||
"name": "Remi"
|
||||
},
|
||||
{
|
||||
"file": "logo-renfeave.svg",
|
||||
"path": "/img/type/logo-renfeave.svg",
|
||||
"path": "/logo-renfeave.svg",
|
||||
"slug": "renfeave",
|
||||
"name": "Renfe AVE"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-a.svg",
|
||||
"path": "/img/type/logo-rer-a.svg",
|
||||
"path": "/logo-rer-a.svg",
|
||||
"slug": "rer-a",
|
||||
"name": "RER A"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-b.svg",
|
||||
"path": "/img/type/logo-rer-b.svg",
|
||||
"path": "/logo-rer-b.svg",
|
||||
"slug": "rer-b",
|
||||
"name": "RER B"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-c.svg",
|
||||
"path": "/img/type/logo-rer-c.svg",
|
||||
"path": "/logo-rer-c.svg",
|
||||
"slug": "rer-c",
|
||||
"name": "RER C"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-d.svg",
|
||||
"path": "/img/type/logo-rer-d.svg",
|
||||
"path": "/logo-rer-d.svg",
|
||||
"slug": "rer-d",
|
||||
"name": "RER D"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-e.svg",
|
||||
"path": "/img/type/logo-rer-e.svg",
|
||||
"path": "/logo-rer-e.svg",
|
||||
"slug": "rer-e",
|
||||
"name": "RER E"
|
||||
},
|
||||
{
|
||||
"file": "logo-sbb.svg",
|
||||
"path": "/img/type/logo-sbb.svg",
|
||||
"path": "/logo-sbb.svg",
|
||||
"slug": "sbb",
|
||||
"name": "SBB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncb.svg",
|
||||
"path": "/img/type/logo-sncb.svg",
|
||||
"path": "/logo-sncb.svg",
|
||||
"slug": "sncb",
|
||||
"name": "SNCB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf.svg",
|
||||
"path": "/img/type/logo-sncf.svg",
|
||||
"path": "/logo-sncf.svg",
|
||||
"slug": "sncf",
|
||||
"name": "Sncf"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/img/type/sncf-voyageurs-logo.png",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"slug": "sncf-voyageurs-logo",
|
||||
"name": "SNCF"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1985.svg",
|
||||
"path": "/img/type/logo-sncf1985.svg",
|
||||
"path": "/logo-sncf1985.svg",
|
||||
"slug": "sncf1985",
|
||||
"name": "Sncf1985"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1992.svg",
|
||||
"path": "/img/type/logo-sncf1992.svg",
|
||||
"path": "/logo-sncf1992.svg",
|
||||
"slug": "sncf1992",
|
||||
"name": "Sncf1992"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-h.svg",
|
||||
"path": "/img/type/logo-t-h.svg",
|
||||
"path": "/logo-t-h.svg",
|
||||
"slug": "t-h",
|
||||
"name": "T H"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-j.svg",
|
||||
"path": "/img/type/logo-t-j.svg",
|
||||
"path": "/logo-t-j.svg",
|
||||
"slug": "t-j",
|
||||
"name": "T J"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-k.svg",
|
||||
"path": "/img/type/logo-t-k.svg",
|
||||
"path": "/logo-t-k.svg",
|
||||
"slug": "t-k",
|
||||
"name": "T K"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-l.svg",
|
||||
"path": "/img/type/logo-t-l.svg",
|
||||
"path": "/logo-t-l.svg",
|
||||
"slug": "t-l",
|
||||
"name": "T L"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-n.svg",
|
||||
"path": "/img/type/logo-t-n.svg",
|
||||
"path": "/logo-t-n.svg",
|
||||
"slug": "t-n",
|
||||
"name": "T N"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-p.svg",
|
||||
"path": "/img/type/logo-t-p.svg",
|
||||
"path": "/logo-t-p.svg",
|
||||
"slug": "t-p",
|
||||
"name": "T P"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-r.svg",
|
||||
"path": "/img/type/logo-t-r.svg",
|
||||
"path": "/logo-t-r.svg",
|
||||
"slug": "t-r",
|
||||
"name": "T R"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-u.svg",
|
||||
"path": "/img/type/logo-t-u.svg",
|
||||
"path": "/logo-t-u.svg",
|
||||
"slug": "t-u",
|
||||
"name": "T U"
|
||||
},
|
||||
{
|
||||
"file": "logo-teoz.svg",
|
||||
"path": "/img/type/logo-teoz.svg",
|
||||
"path": "/logo-teoz.svg",
|
||||
"slug": "teoz",
|
||||
"name": "Teoz"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/img/type/sncf-voyageurs-logo.png",
|
||||
"slug": "ter",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"slug": "TER",
|
||||
"name": "TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-ter-rhone-alpes.svg",
|
||||
"path": "/img/type/logo-ter-rhone-alpes.svg",
|
||||
"path": "/logo-ter-rhone-alpes.svg",
|
||||
"slug": "ter-rhone-alpes",
|
||||
"name": "TER Rhone-Alpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-teralsace.svg",
|
||||
"path": "/img/type/logo-teralsace.svg",
|
||||
"path": "/logo-teralsace.svg",
|
||||
"slug": "teralsace",
|
||||
"name": "Ter Alsace"
|
||||
},
|
||||
{
|
||||
"file": "logo-teraquitaine.svg",
|
||||
"path": "/img/type/logo-teraquitaine.svg",
|
||||
"path": "/logo-teraquitaine.svg",
|
||||
"slug": "teraquitaine",
|
||||
"name": "Ter Aquitaine"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbassenormandie.svg",
|
||||
"path": "/img/type/logo-terbassenormandie.svg",
|
||||
"path": "/logo-terbassenormandie.svg",
|
||||
"slug": "terbassenormandie",
|
||||
"name": "Ter Basse Normandie"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbourgogne.svg",
|
||||
"path": "/img/type/logo-terbourgogne.svg",
|
||||
"path": "/logo-terbourgogne.svg",
|
||||
"slug": "terbourgogne",
|
||||
"name": "Ter Bourgogne"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbretagne.svg",
|
||||
"path": "/img/type/logo-terbretagne.svg",
|
||||
"path": "/logo-terbretagne.svg",
|
||||
"slug": "terbretagne",
|
||||
"name": "Ter Bretagne"
|
||||
},
|
||||
{
|
||||
"file": "logo-tercentre.svg",
|
||||
"path": "/img/type/logo-tercentre.svg",
|
||||
"path": "/logo-tercentre.svg",
|
||||
"slug": "tercentre",
|
||||
"name": "Ter Centre"
|
||||
},
|
||||
{
|
||||
"file": "logo-terlanguedocroussillon.svg",
|
||||
"path": "/img/type/logo-terlanguedocroussillon.svg",
|
||||
"path": "/logo-terlanguedocroussillon.svg",
|
||||
"slug": "terlanguedocroussillon",
|
||||
"name": "Ter Languedoc-Roussillon"
|
||||
},
|
||||
{
|
||||
"file": "logo-termidipyrenees.svg",
|
||||
"path": "/img/type/logo-termidipyrenees.svg",
|
||||
"path": "/logo-termidipyrenees.svg",
|
||||
"slug": "termidipyrenees",
|
||||
"name": "Ter Midi-Pyrenees"
|
||||
},
|
||||
{
|
||||
"file": "logo-ternordpasdecalais.svg",
|
||||
"path": "/img/type/logo-ternordpasdecalais.svg",
|
||||
"path": "/logo-ternordpasdecalais.svg",
|
||||
"slug": "ternordpasdecalais",
|
||||
"name": "Ter Nord-pas-de-Calais"
|
||||
},
|
||||
{
|
||||
"file": "logo-terpoitoucharentes.svg",
|
||||
"path": "/img/type/logo-terpoitoucharentes.svg",
|
||||
"path": "/logo-terpoitoucharentes.svg",
|
||||
"slug": "terpoitoucharentes",
|
||||
"name": "Ter Poitou Charentes"
|
||||
},
|
||||
{
|
||||
"file": "logo-tgv.svg",
|
||||
"path": "/img/type/logo-tgv.svg",
|
||||
"path": "/logo-tgv.svg",
|
||||
"slug": "tgv",
|
||||
"name": "TGV"
|
||||
},
|
||||
{
|
||||
"file": "logo-inoui.svg",
|
||||
"path": "/img/type/logo-inoui.svg",
|
||||
"path": "/logo-inoui.svg",
|
||||
"slug": "inoui",
|
||||
"name": "TGV InOui"
|
||||
},
|
||||
{
|
||||
"file": "logo-lyria.svg",
|
||||
"path": "/img/type/logo-lyria.svg",
|
||||
"path": "/logo-lyria.svg",
|
||||
"slug": "lyria",
|
||||
"name": "TGV Lyria"
|
||||
},
|
||||
{
|
||||
"file": "logo-thalys.svg",
|
||||
"path": "/img/type/logo-thalys.svg",
|
||||
"path": "/logo-thalys.svg",
|
||||
"slug": "thalys",
|
||||
"name": "Thalys"
|
||||
},
|
||||
{
|
||||
"file": "logo-thello.svg",
|
||||
"path": "/img/type/logo-thello.svg",
|
||||
"path": "/logo-thello.svg",
|
||||
"slug": "thello",
|
||||
"name": "Thello"
|
||||
},
|
||||
{
|
||||
"file": "track-car.svg",
|
||||
"path": "/img/type/track-car.svg",
|
||||
"path": "/track-car.svg",
|
||||
"slug": "track-car",
|
||||
"name": "Car TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-tramtrain.svg",
|
||||
"path": "/img/type/logo-tramtrain.svg",
|
||||
"path": "/logo-tramtrain.svg",
|
||||
"slug": "tramtrain",
|
||||
"name": "Tramtrain"
|
||||
},
|
||||
{
|
||||
"file": "logo-trenitalia.svg",
|
||||
"path": "/img/type/logo-trenitalia.svg",
|
||||
"path": "/logo-trenitalia.svg",
|
||||
"slug": "trenitalia",
|
||||
"name": "Trenitalia"
|
||||
},
|
||||
{
|
||||
"file": "logo-zou.svg",
|
||||
"path": "/img/type/logo-zou.svg",
|
||||
"path": "/logo-zou.svg",
|
||||
"slug": "zou",
|
||||
"name": "Zou"
|
||||
}
|
||||
|
||||
@@ -3,181 +3,181 @@
|
||||
"logos": [
|
||||
{
|
||||
"file": "logo-aleop-color.svg",
|
||||
"path": "/img/type/logo-aleop-color.svg",
|
||||
"path": "/logo-aleop-color.svg",
|
||||
"slug": "aleop",
|
||||
"name": "Aleop"
|
||||
},
|
||||
{
|
||||
"file": "logo-auvergerhonealpes-color.svg",
|
||||
"path": "/img/type/logo-auvergerhonealpes-color.svg",
|
||||
"path": "/logo-auvergerhonealpes-color.svg",
|
||||
"slug": "auvergerhonealpes",
|
||||
"name": "Auvergerhonealpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-breizhgo-color.svg",
|
||||
"path": "/img/type/logo-breizhgo-color.svg",
|
||||
"path": "/logo-breizhgo-color.svg",
|
||||
"slug": "breizhgo",
|
||||
"name": "Breizhgo"
|
||||
},
|
||||
{
|
||||
"file": "logo-cfl-color.svg",
|
||||
"path": "/img/type/logo-cfl-color.svg",
|
||||
"path": "/logo-cfl-color.svg",
|
||||
"slug": "cfl",
|
||||
"name": "CFL"
|
||||
},
|
||||
{
|
||||
"file": "logo-db-color.svg",
|
||||
"path": "/img/type/logo-db-color.svg",
|
||||
"path": "/logo-db-color.svg",
|
||||
"slug": "db",
|
||||
"name": "DB"
|
||||
},
|
||||
{
|
||||
"file": "logo-eurostar-color.svg",
|
||||
"path": "/img/type/logo-eurostar-color.svg",
|
||||
"path": "/logo-eurostar-color.svg",
|
||||
"slug": "eurostar",
|
||||
"name": "Eurostar"
|
||||
},
|
||||
{
|
||||
"file": "logo-fluo-color.svg",
|
||||
"path": "/img/type/logo-fluo-color.svg",
|
||||
"path": "/logo-fluo-color.svg",
|
||||
"slug": "fluo",
|
||||
"name": "Fluo"
|
||||
},
|
||||
{
|
||||
"file": "logo-frecciarossa-color.svg",
|
||||
"path": "/img/type/logo-frecciarossa-color.svg",
|
||||
"path": "/logo-frecciarossa-color.svg",
|
||||
"slug": "frecciarossa",
|
||||
"name": "Frecciarossa"
|
||||
},
|
||||
{
|
||||
"file": "logo-hautsdefrance-color.svg",
|
||||
"path": "/img/type/logo-hautsdefrance-color.svg",
|
||||
"path": "/logo-hautsdefrance-color.svg",
|
||||
"slug": "hautsdefrance",
|
||||
"name": "Hautsdefrance"
|
||||
},
|
||||
{
|
||||
"file": "logo-ice-color.svg",
|
||||
"path": "/img/type/logo-ice-color.svg",
|
||||
"path": "/logo-ice-color.svg",
|
||||
"slug": "ice",
|
||||
"name": "ICE"
|
||||
},
|
||||
{
|
||||
"file": "logo-intercites-color.svg",
|
||||
"path": "/img/type/logo-intercites-color.svg",
|
||||
"path": "/logo-intercites-color.svg",
|
||||
"slug": "intercites",
|
||||
"name": "Intercités"
|
||||
},
|
||||
{
|
||||
"file": "logo-ler-color.svg",
|
||||
"path": "/img/type/logo-ler-color.svg",
|
||||
"path": "/logo-ler-color.svg",
|
||||
"slug": "ler",
|
||||
"name": "Ler"
|
||||
},
|
||||
{
|
||||
"file": "logo-lio-color.svg",
|
||||
"path": "/img/type/logo-lio-color.svg",
|
||||
"path": "/logo-lio-color.svg",
|
||||
"slug": "lio",
|
||||
"name": "Lio"
|
||||
},
|
||||
{
|
||||
"file": "logo-lunea-color.svg",
|
||||
"path": "/img/type/logo-lunea-color.svg",
|
||||
"path": "/logo-lunea-color.svg",
|
||||
"slug": "lunea",
|
||||
"name": "Lunea"
|
||||
},
|
||||
{
|
||||
"file": "logo-metrolor-color.svg",
|
||||
"path": "/img/type/logo-metrolor-color.svg",
|
||||
"path": "/logo-metrolor-color.svg",
|
||||
"slug": "metrolor",
|
||||
"name": "Metrolor"
|
||||
},
|
||||
{
|
||||
"file": "logo-mobigo-color.svg",
|
||||
"path": "/img/type/logo-mobigo-color.svg",
|
||||
"path": "/logo-mobigo-color.svg",
|
||||
"slug": "mobigo",
|
||||
"name": "Mobigo"
|
||||
},
|
||||
{
|
||||
"file": "logo-nomad-color.svg",
|
||||
"path": "/img/type/logo-nomad-color.svg",
|
||||
"path": "/logo-nomad-color.svg",
|
||||
"slug": "nomad",
|
||||
"name": "Nomad"
|
||||
},
|
||||
{
|
||||
"file": "logo-occitanie-color.svg",
|
||||
"path": "/img/type/logo-occitanie-color.svg",
|
||||
"path": "/logo-occitanie-color.svg",
|
||||
"slug": "occitanie",
|
||||
"name": "Occitanie"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-color.svg",
|
||||
"path": "/img/type/logo-ouigo-color.svg",
|
||||
"path": "/logo-ouigo-color.svg",
|
||||
"slug": "ouigo",
|
||||
"name": "OUIGO"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-classique-color.svg",
|
||||
"path": "/img/type/logo-ouigo-classique-color.svg",
|
||||
"path": "/logo-ouigo-classique-color.svg",
|
||||
"slug": "ouigo-classique",
|
||||
"name": "OUIGO Trains Classiques"
|
||||
},
|
||||
{
|
||||
"file": "logo-remi-color.svg",
|
||||
"path": "/img/type/logo-remi-color.svg",
|
||||
"path": "/logo-remi-color.svg",
|
||||
"slug": "remi",
|
||||
"name": "Remi"
|
||||
},
|
||||
{
|
||||
"file": "logo-renfeave-color.svg",
|
||||
"path": "/img/type/logo-renfeave-color.svg",
|
||||
"path": "/logo-renfeave-color.svg",
|
||||
"slug": "renfeave",
|
||||
"name": "Renfe AVE"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-a-color.svg",
|
||||
"path": "/img/type/logo-rer-a-color.svg",
|
||||
"path": "/logo-rer-a-color.svg",
|
||||
"slug": "rer-a",
|
||||
"name": "RER A"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-b-color.svg",
|
||||
"path": "/img/type/logo-rer-b-color.svg",
|
||||
"path": "/logo-rer-b-color.svg",
|
||||
"slug": "rer-b",
|
||||
"name": "RER B"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-c-color.svg",
|
||||
"path": "/img/type/logo-rer-c-color.svg",
|
||||
"path": "/logo-rer-c-color.svg",
|
||||
"slug": "rer-c",
|
||||
"name": "RER C"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-d-color.svg",
|
||||
"path": "/img/type/logo-rer-d-color.svg",
|
||||
"path": "/logo-rer-d-color.svg",
|
||||
"slug": "rer-d",
|
||||
"name": "RER D"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-e-color.svg",
|
||||
"path": "/img/type/logo-rer-e-color.svg",
|
||||
"path": "/logo-rer-e-color.svg",
|
||||
"slug": "rer-e",
|
||||
"name": "RER E"
|
||||
},
|
||||
{
|
||||
"file": "logo-sbb-color.svg",
|
||||
"path": "/img/type/logo-sbb-color.svg",
|
||||
"path": "/logo-sbb-color.svg",
|
||||
"slug": "sbb",
|
||||
"name": "SBB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncb-color.svg",
|
||||
"path": "/img/type/logo-sncb-color.svg",
|
||||
"path": "/logo-sncb-color.svg",
|
||||
"slug": "sncb",
|
||||
"name": "SNCB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf-color.svg",
|
||||
"path": "/img/type/logo-sncf-color.svg",
|
||||
"path": "/logo-sncf-color.svg",
|
||||
"slug": "sncf",
|
||||
"name": "Sncf"
|
||||
},
|
||||
@@ -255,127 +255,127 @@
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/img/type/sncf-voyageurs-logo.png",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"slug": "ter",
|
||||
"name": "TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-ter-rhone-alpes.svg",
|
||||
"path": "/img/type/logo-ter-rhone-alpes.svg",
|
||||
"path": "/logo-ter-rhone-alpes.svg",
|
||||
"slug": "ter-rhone-alpes",
|
||||
"name": "TER Rhone Alpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-teralsace.svg",
|
||||
"path": "/img/type/logo-teralsace.svg",
|
||||
"path": "/logo-teralsace.svg",
|
||||
"slug": "teralsace",
|
||||
"name": "Teralsace"
|
||||
},
|
||||
{
|
||||
"file": "logo-teraquitaine.svg",
|
||||
"path": "/img/type/logo-teraquitaine.svg",
|
||||
"path": "/logo-teraquitaine.svg",
|
||||
"slug": "teraquitaine",
|
||||
"name": "Teraquitaine"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbassenormandie.svg",
|
||||
"path": "/img/type/logo-terbassenormandie.svg",
|
||||
"path": "/logo-terbassenormandie.svg",
|
||||
"slug": "terbassenormandie",
|
||||
"name": "Terbassenormandie"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbourgogne.svg",
|
||||
"path": "/img/type/logo-terbourgogne.svg",
|
||||
"path": "/logo-terbourgogne.svg",
|
||||
"slug": "terbourgogne",
|
||||
"name": "Terbourgogne"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbretagne.svg",
|
||||
"path": "/img/type/logo-terbretagne.svg",
|
||||
"path": "/logo-terbretagne.svg",
|
||||
"slug": "terbretagne",
|
||||
"name": "Terbretagne"
|
||||
},
|
||||
{
|
||||
"file": "logo-tercentre.svg",
|
||||
"path": "/img/type/logo-tercentre.svg",
|
||||
"path": "/logo-tercentre.svg",
|
||||
"slug": "tercentre",
|
||||
"name": "Tercentre"
|
||||
},
|
||||
{
|
||||
"file": "logo-terlanguedocroussillon.svg",
|
||||
"path": "/img/type/logo-terlanguedocroussillon.svg",
|
||||
"path": "/logo-terlanguedocroussillon.svg",
|
||||
"slug": "terlanguedocroussillon",
|
||||
"name": "Terlanguedocroussillon"
|
||||
},
|
||||
{
|
||||
"file": "logo-termidipyrenees.svg",
|
||||
"path": "/img/type/logo-termidipyrenees.svg",
|
||||
"path": "/logo-termidipyrenees.svg",
|
||||
"slug": "termidipyrenees",
|
||||
"name": "Termidipyrenees"
|
||||
},
|
||||
{
|
||||
"file": "logo-ternordpasdecalais.svg",
|
||||
"path": "/img/type/logo-ternordpasdecalais.svg",
|
||||
"path": "/logo-ternordpasdecalais.svg",
|
||||
"slug": "ternordpasdecalais",
|
||||
"name": "Ternordpasdecalais"
|
||||
},
|
||||
{
|
||||
"file": "logo-terpoitoucharentes.svg",
|
||||
"path": "/img/type/logo-terpoitoucharentes.svg",
|
||||
"path": "/logo-terpoitoucharentes.svg",
|
||||
"slug": "terpoitoucharentes",
|
||||
"name": "Terpoitoucharentes"
|
||||
},
|
||||
{
|
||||
"file": "logo-tgv.svg",
|
||||
"path": "/img/type/logo-tgv.svg",
|
||||
"path": "/logo-tgv.svg",
|
||||
"slug": "tgv",
|
||||
"name": "TGV"
|
||||
},
|
||||
{
|
||||
"file": "logo-inoui.svg",
|
||||
"path": "/img/type/logo-inoui.svg",
|
||||
"path": "/logo-inoui.svg",
|
||||
"slug": "inoui",
|
||||
"name": "TGV InOui"
|
||||
},
|
||||
{
|
||||
"file": "logo-lyria.svg",
|
||||
"path": "/img/type/logo-lyria.svg",
|
||||
"path": "/logo-lyria.svg",
|
||||
"slug": "lyria",
|
||||
"name": "TGV Lyria"
|
||||
},
|
||||
{
|
||||
"file": "logo-thalys.svg",
|
||||
"path": "/img/type/logo-thalys.svg",
|
||||
"path": "/logo-thalys.svg",
|
||||
"slug": "thalys",
|
||||
"name": "Thalys"
|
||||
},
|
||||
{
|
||||
"file": "logo-thello.svg",
|
||||
"path": "/img/type/logo-thello.svg",
|
||||
"path": "/logo-thello.svg",
|
||||
"slug": "thello",
|
||||
"name": "Thello"
|
||||
},
|
||||
{
|
||||
"file": "track-car.svg",
|
||||
"path": "/img/type/track-car.svg",
|
||||
"path": "/track-car.svg",
|
||||
"slug": "track-car",
|
||||
"name": "Track Car"
|
||||
},
|
||||
{
|
||||
"file": "logo-tramtrain.svg",
|
||||
"path": "/img/type/logo-tramtrain.svg",
|
||||
"path": "/logo-tramtrain.svg",
|
||||
"slug": "tramtrain",
|
||||
"name": "Tramtrain"
|
||||
},
|
||||
{
|
||||
"file": "logo-trenitalia.svg",
|
||||
"path": "/img/type/logo-trenitalia.svg",
|
||||
"path": "/logo-trenitalia.svg",
|
||||
"slug": "trenitalia",
|
||||
"name": "Trenitalia"
|
||||
},
|
||||
{
|
||||
"file": "logo-zou.svg",
|
||||
"path": "/img/type/logo-zou.svg",
|
||||
"path": "/logo-zou.svg",
|
||||
"slug": "zou",
|
||||
"name": "Zou"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
+25
@@ -11,6 +11,31 @@
|
||||
|
||||
/* Styles responsive pour les pages de guidage & sélection */
|
||||
@import "./styles/Launcher.css";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Avenir;
|
||||
src: url("/public/fonts/avenir-lighter.woff");
|
||||
|
||||
+18
@@ -9,6 +9,8 @@ import ClassiquesDeparts from './pages/afficheurs/classiques/ClassiquesDeparts';
|
||||
import ClassiquesArrivees from './pages/afficheurs/classiques/ClassiquesArrivees';
|
||||
import ClassiquesDepartsQuais from './pages/afficheurs/classiques/quais/ClassiquesDepartsQuais';
|
||||
import ClassiquesArriveesQuais from './pages/afficheurs/classiques/quais/ClassiquesArriveesQuais';
|
||||
import ClassiquesFlashInfo from './pages/afficheurs/classiques/ClassiquesFlashInfo';
|
||||
import ClassiquesInformations from './pages/afficheurs/classiques/ClassiquesInformations';
|
||||
import AFLDeparts from './pages/afficheurs/afl/AFLDeparts';
|
||||
import AFLArrivees from './pages/afficheurs/afl/AFLArrivees';
|
||||
import AFLDepartsQuais from './pages/afficheurs/afl/AFLDepartsQuais';
|
||||
@@ -198,6 +200,22 @@ function App() {
|
||||
}
|
||||
|
||||
// Routes pour afficheurs : supporte le mode racine ET le mode /ferrovia
|
||||
if (
|
||||
cleanRoute.startsWith('/afficheurs/classiques/flash-info') ||
|
||||
cleanRoute.startsWith('/afficheurs/classiques/flash-circulation') ||
|
||||
cleanRoute.startsWith('/afficheurs/departs/classiques/flash-info') ||
|
||||
cleanRoute.startsWith('/afficheurs/arrivees/classiques/flash-info')
|
||||
) {
|
||||
return <AfficheurViewportWrapper><ClassiquesFlashInfo /></AfficheurViewportWrapper>;
|
||||
}
|
||||
if (
|
||||
cleanRoute.startsWith('/afficheurs/classiques/informations') ||
|
||||
cleanRoute.startsWith('/afficheurs/classiques/infos') ||
|
||||
cleanRoute.startsWith('/afficheurs/departs/classiques/informations') ||
|
||||
cleanRoute.startsWith('/afficheurs/arrivees/classiques/informations')
|
||||
) {
|
||||
return <AfficheurViewportWrapper><ClassiquesInformations /></AfficheurViewportWrapper>;
|
||||
}
|
||||
if (cleanRoute.startsWith('/afficheurs/departs/classiques/quais')) {
|
||||
return <AfficheurViewportWrapper><ClassiquesDepartsQuais /></AfficheurViewportWrapper>;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import './MarqueeText.css';
|
||||
|
||||
const MarqueeText = ({ text, className, style }) => {
|
||||
const DEFAULT_SPEED = 50; // pixels per second for uniform, calm scrolling
|
||||
|
||||
const MarqueeText = ({ text, className, style, speed = DEFAULT_SPEED, duration }) => {
|
||||
const containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
const [computedDuration, setComputedDuration] = useState('20s');
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
const updateMarquee = () => {
|
||||
if (containerRef.current && textRef.current) {
|
||||
setIsOverflowing(textRef.current.scrollWidth > containerRef.current.clientWidth);
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
const textWidth = textRef.current.scrollWidth;
|
||||
const overflowing = textWidth > containerWidth;
|
||||
setIsOverflowing(overflowing);
|
||||
|
||||
if (overflowing) {
|
||||
if (duration) {
|
||||
setComputedDuration(typeof duration === 'number' ? `${duration}s` : duration);
|
||||
} else {
|
||||
const totalDistance = textWidth + containerWidth;
|
||||
const seconds = totalDistance / speed;
|
||||
setComputedDuration(`${seconds.toFixed(2)}s`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
checkOverflow();
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
return () => window.removeEventListener('resize', checkOverflow);
|
||||
}, [text]);
|
||||
|
||||
updateMarquee();
|
||||
window.addEventListener('resize', updateMarquee);
|
||||
const timer = setTimeout(updateMarquee, 100);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateMarquee);
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [text, speed, duration]);
|
||||
|
||||
return (
|
||||
<div className={`marquee-container ${className || ''}`} ref={containerRef} style={{ ...style, overflow: 'hidden', whiteSpace: 'nowrap', width: '100%', flex: '1 1 0' }}>
|
||||
@@ -24,7 +45,7 @@ const MarqueeText = ({ text, className, style }) => {
|
||||
ref={textRef}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
animation: isOverflowing ? 'marquee-scroll 10s linear infinite' : 'none',
|
||||
animation: isOverflowing ? `marquee-scroll ${computedDuration} linear infinite` : 'none',
|
||||
paddingLeft: isOverflowing ? '100%' : '0',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
.aff-attentifs__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
user-select: none;
|
||||
font-family: 'Achemine', 'Arial', sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* COLONNE GAUCHE (36%) */
|
||||
.aff-attentifs__left-col {
|
||||
flex: 0 0 36%;
|
||||
background-color: #313131;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 70px 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-attentifs__title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aff-attentifs__title-word {
|
||||
font-size: 78px;
|
||||
font-weight: 900;
|
||||
color: #fdb913;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1.05;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aff-attentifs__vigipirate-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.aff-attentifs__vigipirate-svg {
|
||||
width: 270px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
/* COLONNE DROITE (64%) */
|
||||
.aff-attentifs__right-col {
|
||||
flex: 1;
|
||||
background-color: #4a4a4a;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 60px 80px;
|
||||
box-sizing: border-box;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.aff-attentifs__info-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-attentifs__info-desc {
|
||||
font-size: 27px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 36px 0;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.aff-attentifs__call-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.aff-attentifs__call-text {
|
||||
font-size: 44px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-attentifs__call-hours {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #eaeaea;
|
||||
}
|
||||
|
||||
.aff-attentifs__call-footnote {
|
||||
font-size: 19px;
|
||||
font-weight: 400;
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
/* SECTION BULLE D'ALERTE 3117 */
|
||||
.aff-attentifs__bubble-section {
|
||||
flex: 0 0 460px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-card {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border: 7px solid #e2001a;
|
||||
border-radius: 36px;
|
||||
padding: 30px 42px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-title {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #e2001a;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-label {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
color: #e2001a;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-number {
|
||||
font-size: 58px;
|
||||
font-weight: 900;
|
||||
color: #e2001a;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-tail {
|
||||
position: absolute;
|
||||
bottom: -32px;
|
||||
left: 54px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 20px solid transparent;
|
||||
border-right: 20px solid transparent;
|
||||
border-top: 32px solid #e2001a;
|
||||
}
|
||||
|
||||
.aff-attentifs__bubble-tail::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -38px;
|
||||
left: -13px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 13px solid transparent;
|
||||
border-right: 13px solid transparent;
|
||||
border-top: 24px solid #ffffff;
|
||||
}
|
||||
|
||||
/* SMARTPHONE BADGE */
|
||||
.aff-attentifs__phone-badge {
|
||||
position: absolute;
|
||||
top: -34px;
|
||||
right: -36px;
|
||||
z-index: 10;
|
||||
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.aff-attentifs__phone-svg {
|
||||
width: 86px;
|
||||
height: auto;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import './AttentifsEnsemble.css';
|
||||
|
||||
export default function AttentifsEnsemble() {
|
||||
return (
|
||||
<div className="aff-attentifs__container">
|
||||
{/* COLONNE GAUCHE : ANTHRACITE FONCÉ (ATTENTIFS ENSEMBLE + VIGIPIRATE) */}
|
||||
<div className="aff-attentifs__left-col">
|
||||
<div className="aff-attentifs__title-group">
|
||||
<div className="aff-attentifs__title-word">ATTENTIFS</div>
|
||||
<div className="aff-attentifs__title-word">ENSEMBLE</div>
|
||||
</div>
|
||||
|
||||
{/* Insigne Vigipirate Officiel */}
|
||||
<div className="aff-attentifs__vigipirate-wrap">
|
||||
<svg
|
||||
viewBox="0 0 280 240"
|
||||
className="aff-attentifs__vigipirate-svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Triangle noir extérieur */}
|
||||
<polygon points="140,12 268,228 12,228" fill="#1e1e1e" stroke="#1e1e1e" strokeWidth="8" strokeLinejoin="round" />
|
||||
{/* Triangle rouge intérieur */}
|
||||
<polygon points="140,42 238,206 42,206" fill="#e2001a" />
|
||||
{/* Bandeau blanc en bas */}
|
||||
<polygon points="56,182 224,182 238,206 42,206" fill="#ffffff" />
|
||||
{/* Texte VIGIPIRATE */}
|
||||
<text
|
||||
x="140"
|
||||
y="201"
|
||||
fill="#1e1e1e"
|
||||
fontSize="23"
|
||||
fontWeight="900"
|
||||
fontFamily="Achemine, Arial, sans-serif"
|
||||
textAnchor="middle"
|
||||
letterSpacing="2.5"
|
||||
>
|
||||
VIGIPIRATE
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COLONNE DROITE : GRIS MOYEN (CONSIGNES + BULLE ALERTE 3117) */}
|
||||
<div className="aff-attentifs__right-col">
|
||||
{/* Partie gauche des consignes */}
|
||||
<div className="aff-attentifs__info-section">
|
||||
<p className="aff-attentifs__info-desc">
|
||||
Si vous êtes témoin<br />
|
||||
d'une situation<br />
|
||||
qui présente un risque<br />
|
||||
pour votre sécurité<br />
|
||||
ou celle des autres<br />
|
||||
voyageurs,
|
||||
</p>
|
||||
|
||||
<div className="aff-attentifs__call-action">
|
||||
<span className="aff-attentifs__call-text">APPELER* LE 3117</span>
|
||||
<span className="aff-attentifs__call-hours">24h/24 · 7j/7</span>
|
||||
</div>
|
||||
|
||||
<div className="aff-attentifs__call-footnote">
|
||||
* Appel non surtaxé et enregistré
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Partie droite avec la bulle Alerte SNCF 3117 et le smartphone */}
|
||||
<div className="aff-attentifs__bubble-section">
|
||||
<div className="aff-attentifs__bubble-wrap">
|
||||
{/* Bulle blanche à bordure rouge */}
|
||||
<div className="aff-attentifs__bubble-card">
|
||||
<div className="aff-attentifs__bubble-title">ALERTE SNCF</div>
|
||||
<div className="aff-attentifs__bubble-rows">
|
||||
<div className="aff-attentifs__bubble-line">
|
||||
<span className="aff-attentifs__bubble-label">TÉL.</span>
|
||||
<span className="aff-attentifs__bubble-number">3117*</span>
|
||||
</div>
|
||||
<div className="aff-attentifs__bubble-line">
|
||||
<span className="aff-attentifs__bubble-label">SMS</span>
|
||||
<span className="aff-attentifs__bubble-number">31177**</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pointe de la bulle */}
|
||||
<div className="aff-attentifs__bubble-tail" />
|
||||
</div>
|
||||
|
||||
{/* Smartphone Rouge d'alerte */}
|
||||
<div className="aff-attentifs__phone-badge">
|
||||
<svg viewBox="0 0 68 104" className="aff-attentifs__phone-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Corps de téléphone rouge arrondi */}
|
||||
<rect x="4" y="4" width="60" height="96" rx="14" fill="#e2001a" stroke="#ffffff" strokeWidth="4.5" />
|
||||
{/* Écran blanc */}
|
||||
<rect x="12" y="14" width="44" height="50" rx="6" fill="#ffffff" />
|
||||
{/* Point d'exclamation rouge d'urgence */}
|
||||
<rect x="30.5" y="24" width="7" height="20" rx="3.5" fill="#e2001a" />
|
||||
<circle cx="34" cy="53" r="4" fill="#e2001a" />
|
||||
{/* Touches du pavé numérique */}
|
||||
<circle cx="20" cy="74" r="3" fill="#ffffff" />
|
||||
<circle cx="34" cy="74" r="3" fill="#ffffff" />
|
||||
<circle cx="48" cy="74" r="3" fill="#ffffff" />
|
||||
<circle cx="20" cy="85" r="3" fill="#ffffff" />
|
||||
<circle cx="34" cy="85" r="3" fill="#ffffff" />
|
||||
<circle cx="48" cy="85" r="3" fill="#ffffff" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
.aff-dep-afl__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #d61e38;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
font-family: 'Achemine', 'Arial', sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* FRISSE SUPÉRIEURE RAYÉE */
|
||||
.aff-dep-afl__hazard-stripes {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#ffffff,
|
||||
#ffffff 28px,
|
||||
#d61e38 28px,
|
||||
#d61e38 56px
|
||||
);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* CONTENU PRINCIPAL */
|
||||
.aff-dep-afl__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 100px;
|
||||
gap: 120px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-dep-afl__icon-col {
|
||||
flex: 0 0 460px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-dep-afl__no-entry-svg {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.aff-dep-afl__text-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.aff-dep-afl__heading-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.aff-dep-afl__title-main,
|
||||
.aff-dep-afl__title-sub {
|
||||
font-size: 102px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
color: #ffffff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aff-dep-afl__instructions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aff-dep-afl__instruction-line {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import './DepartEnCoursAFL.css';
|
||||
|
||||
export default function DepartEnCoursAFL() {
|
||||
return (
|
||||
<div className="aff-dep-afl__container">
|
||||
{/* RUBAN SUPÉRIEUR RAYÉ BLANC ET ROUGE */}
|
||||
<div className="aff-dep-afl__hazard-stripes" />
|
||||
|
||||
{/* CONTENU PRINCIPAL */}
|
||||
<div className="aff-dep-afl__content">
|
||||
{/* Panneau d'interdiction blanc à gauche */}
|
||||
<div className="aff-dep-afl__icon-col">
|
||||
<svg
|
||||
viewBox="0 0 240 240"
|
||||
className="aff-dep-afl__no-entry-svg"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Cercle blanc extérieur */}
|
||||
<circle cx="120" cy="120" r="105" stroke="#ffffff" strokeWidth="18" />
|
||||
|
||||
{/* Silhouette du piéton marchant avec valise */}
|
||||
<g fill="#ffffff">
|
||||
{/* Tête */}
|
||||
<circle cx="126" cy="62" r="15" />
|
||||
{/* Corps marchant */}
|
||||
<path d="M116 84 C116 84, 134 84, 138 98 L152 144 L140 148 L128 110 L118 132 L112 186 L98 184 L106 126 L96 112 Z" />
|
||||
{/* Jambe droite enjambant */}
|
||||
<path d="M122 134 L138 180 C138 180, 146 186, 140 190 C134 194, 126 188, 126 188 L114 148 Z" />
|
||||
{/* Bras gauche */}
|
||||
<path d="M128 88 L146 114 L138 120 L122 96 Z" />
|
||||
{/* Bras droit tenant la valise */}
|
||||
<path d="M116 86 L98 116 L88 110" stroke="#ffffff" strokeWidth="8" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
{/* Valise inclinée */}
|
||||
<g transform="rotate(-15, 80, 140)">
|
||||
<rect x="58" y="122" width="38" height="52" rx="7" fill="#ffffff" />
|
||||
<path d="M70 122 V112 H84 V122" stroke="#ffffff" strokeWidth="6" fill="none" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{/* Barre diagonale blanche d'interdiction */}
|
||||
<line x1="46" y1="46" x2="194" y2="194" stroke="#ffffff" strokeWidth="18" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Textes d'alerte et de consigne à droite */}
|
||||
<div className="aff-dep-afl__text-col">
|
||||
<div className="aff-dep-afl__heading-group">
|
||||
<div className="aff-dep-afl__title-main">DÉPARTS</div>
|
||||
<div className="aff-dep-afl__title-sub">EN COURS</div>
|
||||
</div>
|
||||
|
||||
<div className="aff-dep-afl__instructions">
|
||||
<p className="aff-dep-afl__instruction-line">Pour votre sécurité</p>
|
||||
<p className="aff-dep-afl__instruction-line">merci de vous éloigner</p>
|
||||
<p className="aff-dep-afl__instruction-line">de la bordure du quai.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.aff-dep-classique__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
font-family: 'Achemine', 'Arial', sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ZONE DU HAUT : GRIS FONCÉ (~72%) */
|
||||
.aff-dep-classique__top {
|
||||
flex: 1;
|
||||
background-color: #222226;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 160px;
|
||||
padding: 40px 100px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aff-dep-classique__prohibition-sign {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-dep-classique__no-entry-svg {
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
.aff-dep-classique__train-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-dep-classique__train-gif {
|
||||
width: 480px;
|
||||
height: auto;
|
||||
max-height: 480px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
.aff-dep-classique__train-svg {
|
||||
width: 460px;
|
||||
height: 460px;
|
||||
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
/* BANDEAU ROUGE INFÉRIEUR (~28%) */
|
||||
.aff-dep-classique__bottom {
|
||||
flex: 0 0 280px;
|
||||
background-color: #d0102a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.aff-dep-classique__banner-text {
|
||||
font-size: 110px;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react';
|
||||
import './DepartEnCoursClassique.css';
|
||||
|
||||
const GIF_URL = 'https://cdn.discordapp.com/attachments/1265325000290992258/1282018214720835725/SNCF_-_Depart_Train.gif?ex=6a95af01&is=6a945d81&hm=14570f1423217cb52437d767b3125a698ac5955fc088028fbc3d24d820e9a76b&';
|
||||
|
||||
export default function DepartEnCoursClassique() {
|
||||
const [gifError, setGifError] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="aff-dep-classique__container">
|
||||
{/* ZONE SUPÉRIEURE : PICTOS (FOND GRIS FONCÉ) */}
|
||||
<div className="aff-dep-classique__top">
|
||||
{/* Panneau d'interdiction : Ne pas monter (voyageur avec valise barré) */}
|
||||
<div className="aff-dep-classique__prohibition-sign">
|
||||
<svg viewBox="0 0 200 200" className="aff-dep-classique__no-entry-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Fond blanc du cercle */}
|
||||
<circle cx="100" cy="100" r="90" fill="#ffffff" stroke="#d0102a" strokeWidth="18" />
|
||||
|
||||
{/* Silhouette du voyageur avec valise */}
|
||||
<g fill="#141416">
|
||||
{/* Tête */}
|
||||
<circle cx="92" cy="52" r="14" />
|
||||
{/* Corps */}
|
||||
<rect x="76" y="70" width="32" height="52" rx="10" />
|
||||
{/* Jambe gauche */}
|
||||
<rect x="80" y="118" width="10" height="38" rx="5" />
|
||||
{/* Jambe droite */}
|
||||
<rect x="94" y="118" width="10" height="38" rx="5" />
|
||||
{/* Bras gauche */}
|
||||
<rect x="66" y="72" width="9" height="36" rx="4.5" />
|
||||
{/* Bras droit tenant la poignée */}
|
||||
<path d="M106 74 L120 102 L120 114" stroke="#141416" strokeWidth="8" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
{/* Poignée télescopique de valise */}
|
||||
<path d="M116 102 V116 H124 V102" stroke="#141416" strokeWidth="4" fill="none" />
|
||||
{/* Valise à roulettes */}
|
||||
<rect x="110" y="116" width="24" height="38" rx="4" fill="#141416" />
|
||||
{/* Roulettes */}
|
||||
<circle cx="114" cy="156" r="3" fill="#141416" />
|
||||
<circle cx="130" cy="156" r="3" fill="#141416" />
|
||||
</g>
|
||||
|
||||
{/* Barre diagonale rouge d'interdiction */}
|
||||
<line x1="36" y1="36" x2="164" y2="164" stroke="#d0102a" strokeWidth="18" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Animation du train au départ (GIF ou SVG de secours) */}
|
||||
<div className="aff-dep-classique__train-wrap">
|
||||
{!gifError ? (
|
||||
<img
|
||||
src={GIF_URL}
|
||||
alt="Train au départ"
|
||||
className="aff-dep-classique__train-gif"
|
||||
onError={() => setGifError(true)}
|
||||
/>
|
||||
) : (
|
||||
<svg viewBox="0 0 240 240" className="aff-dep-classique__train-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* Rails et traverses */}
|
||||
<path d="M40 200 L95 140 M200 200 L145 140" stroke="#ffffff" strokeWidth="8" strokeLinecap="round" />
|
||||
<line x1="60" y1="185" x2="180" y2="185" stroke="#ffffff" strokeWidth="8" strokeLinecap="round" />
|
||||
<line x1="85" y1="160" x2="155" y2="160" stroke="#ffffff" strokeWidth="6" strokeLinecap="round" />
|
||||
|
||||
{/* Face avant du train */}
|
||||
<rect x="55" y="40" width="130" height="110" rx="35" fill="#ffffff" />
|
||||
{/* Pare-brise */}
|
||||
<rect x="68" y="52" width="104" height="42" rx="14" fill="#1c1c20" />
|
||||
{/* Phares rouges */}
|
||||
<circle cx="82" cy="120" r="10" fill="#d0102a" />
|
||||
<circle cx="158" cy="120" r="10" fill="#d0102a" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BANDEAU INFÉRIEUR ROUGE : DÉPART EN COURS */}
|
||||
<div className="aff-dep-classique__bottom">
|
||||
<h1 className="aff-dep-classique__banner-text">Départ en cours</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
.aff-videoprot__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #141416;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 80px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Achemine', 'Arial', sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.aff-videoprot__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 80px;
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.aff-videoprot__icon-col {
|
||||
flex: 0 0 420px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-videoprot__camera-svg {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 6px 16px rgba(242, 91, 42, 0.25));
|
||||
}
|
||||
|
||||
.aff-videoprot__text-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-videoprot__title-vigi {
|
||||
font-size: 52px;
|
||||
font-weight: 800;
|
||||
color: #f25b2a;
|
||||
letter-spacing: 1.5px;
|
||||
margin-bottom: 24px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aff-videoprot__main-heading {
|
||||
font-size: 46px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
line-height: 1.28;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 54px 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aff-videoprot__subtext-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aff-videoprot__legal-line,
|
||||
.aff-videoprot__access-line {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import './GareVideoprotection.css';
|
||||
|
||||
export default function GareVideoprotection() {
|
||||
return (
|
||||
<div className="aff-videoprot__container">
|
||||
<div className="aff-videoprot__wrapper">
|
||||
{/* Caméra de surveillance stylisée orange */}
|
||||
<div className="aff-videoprot__icon-col">
|
||||
<svg
|
||||
viewBox="0 0 320 240"
|
||||
className="aff-videoprot__camera-svg"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Support mural */}
|
||||
<rect x="20" y="105" width="22" height="75" rx="5" fill="#f25b2a" />
|
||||
<path
|
||||
d="M38 132 H80 L95 105 H125"
|
||||
stroke="#f25b2a"
|
||||
strokeWidth="20"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Corps de la caméra orienté vers le bas à droite */}
|
||||
<g transform="rotate(18, 140, 110)">
|
||||
{/* Corps principal */}
|
||||
<rect x="35" y="55" width="165" height="70" rx="14" fill="#f25b2a" />
|
||||
{/* Parasoleil / Visière supérieure */}
|
||||
<path d="M30 48 L210 48 L195 58 L30 58 Z" fill="#f25b2a" />
|
||||
{/* Cône avant / Objectif */}
|
||||
<polygon points="198,62 250,38 250,142 198,118" fill="#f25b2a" />
|
||||
{/* Fentes d'aération & détails noirs */}
|
||||
<circle cx="65" cy="88" r="10" fill="#141416" />
|
||||
<rect x="95" y="78" width="10" height="26" rx="4" fill="#141416" />
|
||||
<rect x="120" y="78" width="10" height="26" rx="4" fill="#141416" />
|
||||
<rect x="145" y="78" width="10" height="26" rx="4" fill="#141416" />
|
||||
{/* Bague optique */}
|
||||
<rect x="250" y="44" width="14" height="92" rx="3" fill="#f25b2a" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Textes d'information et réglementaires */}
|
||||
<div className="aff-videoprot__text-col">
|
||||
<div className="aff-videoprot__title-vigi">VIGI360</div>
|
||||
|
||||
<h1 className="aff-videoprot__main-heading">
|
||||
POUR VOTRE SÉCURITÉ, CET ESPACE<br />
|
||||
EST ÉQUIPÉ DE VIDÉOPROTECTION.
|
||||
</h1>
|
||||
|
||||
<div className="aff-videoprot__subtext-group">
|
||||
<p className="aff-videoprot__legal-line">
|
||||
En application des dispositions des textes législatifs et<br />
|
||||
règlementaires en vigueur.
|
||||
</p>
|
||||
<p className="aff-videoprot__access-line">
|
||||
Pour l'exercice du droit d'accès au enregistrements,<br />
|
||||
s'adresser à un agent d'accueil de la gare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/* =========================================================
|
||||
BANDEAU COMPOSITION TGV (SNCF QUAI - Conforme Image 1 & 2)
|
||||
========================================================= */
|
||||
|
||||
.aff-tgv-comp__container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 240px;
|
||||
background-color: #4a0e4e; /* Couleur Prune / Aubergine SNCF authentique */
|
||||
color: #ffffff;
|
||||
font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 35px 8px 35px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
z-index: 25;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
TOP ROW : BOX VOIE + RAMES TGV
|
||||
========================================================= */
|
||||
.aff-tgv-comp__top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
/* Box Voie à gauche */
|
||||
.aff-tgv-comp__track-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__track-pill {
|
||||
background-color: #ffffff;
|
||||
color: #4a0e4e;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 14px;
|
||||
border-radius: 12px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__track-letter {
|
||||
color: #ffffff;
|
||||
font-size: 64px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__track-underline {
|
||||
width: 65px;
|
||||
height: 3.5px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Conteneur des rames TGV */
|
||||
.aff-tgv-comp__rames-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 40px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Bloc d'une Rame */
|
||||
.aff-tgv-comp__rame-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Titre Destination au-dessus de la rame (jaune doré) */
|
||||
.aff-tgv-comp__dest-title {
|
||||
color: #f5d130;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Ligne des numéros de voitures */
|
||||
.aff-tgv-comp__coaches-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-num-motrice {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-num-box {
|
||||
flex: 1;
|
||||
min-width: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Graphic de la rame TGV */
|
||||
.aff-tgv-comp__train-graphic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Motrice avant */
|
||||
.aff-tgv-comp__loco-front {
|
||||
width: 44px;
|
||||
height: 30px;
|
||||
background: linear-gradient(180deg, #813988 0%, #5d1d64 100%);
|
||||
border-top-left-radius: 16px;
|
||||
border-bottom-left-radius: 12px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Corps des voitures */
|
||||
.aff-tgv-comp__coaches-body {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-segment {
|
||||
flex: 1;
|
||||
min-width: 44px;
|
||||
height: 30px;
|
||||
background-color: #b776bd;
|
||||
border-right: 2px solid #ffffff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-segment:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-segment:last-child {
|
||||
border-right: none;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__coach-segment--bar {
|
||||
background-color: #c98cd0;
|
||||
}
|
||||
|
||||
/* Motrice arrière */
|
||||
.aff-tgv-comp__loco-rear {
|
||||
width: 44px;
|
||||
height: 30px;
|
||||
background: linear-gradient(180deg, #813988 0%, #5d1d64 100%);
|
||||
border-top-right-radius: 16px;
|
||||
border-bottom-right-radius: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-tgv-comp__reperes-line-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 2px 0 4px 0;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__dotted-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 2.5px dotted #f5d130;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__reperes-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 36px;
|
||||
width: 100%;
|
||||
padding-right: 40px;
|
||||
box-sizing: border-box;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__repere-box {
|
||||
background-color: #00337f;
|
||||
border: 1.5px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
font-size: 21px;
|
||||
line-height: 1;
|
||||
width: 32px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
BOTTOM ROW : MARQUEE + DIGITAL CLOCK
|
||||
========================================================= */
|
||||
.aff-tgv-comp__bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__marquee-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__marquee-text {
|
||||
display: inline-block;
|
||||
color: #ffffff;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-size: 29px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
animation: affTgvMarquee 25s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes affTgvMarquee {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Horloge TGV */
|
||||
.aff-tgv-comp__clock-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__clock {
|
||||
background-color: #00337f;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 3px 14px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__clock-hm {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-tgv-comp__clock-s {
|
||||
color: #f5a623;
|
||||
margin-left: 2px;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './TgvCompositionQuai.css';
|
||||
|
||||
/**
|
||||
* Icône Bar / Coupe de verre
|
||||
*/
|
||||
function BarGlassIcon({ size = 20, color = '#ffffff' }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4L12 14L20 4H4Z" stroke={color} strokeWidth="2.2" strokeLinejoin="round" fill={color} />
|
||||
<path d="M12 14V21M8 21H16" stroke={color} strokeWidth="2.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Horloge digitale avec secondes (conformité Image 1 & 2)
|
||||
*/
|
||||
function TgvDigitalClock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
return (
|
||||
<div className="aff-tgv-comp__clock">
|
||||
<span className="aff-tgv-comp__clock-hm">{hh}:{mm}</span>
|
||||
<span className="aff-tgv-comp__clock-s">{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dessin d'une rame TGV de 8 voitures avec motrice avant, arrière et voiture Bar en position 4
|
||||
*/
|
||||
function TgvRameShape({ destination, coaches = ['01', '02', '03', 'bar', '05', '06', '07', '08'] }) {
|
||||
return (
|
||||
<div className="aff-tgv-comp__rame-block">
|
||||
{destination && (
|
||||
<div className="aff-tgv-comp__dest-title">
|
||||
{destination}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Numéros des voitures */}
|
||||
<div className="aff-tgv-comp__coaches-row">
|
||||
{/* Espace motrice avant */}
|
||||
<div className="aff-tgv-comp__coach-num-motrice" />
|
||||
|
||||
{coaches.map((c, idx) => (
|
||||
<div key={idx} className="aff-tgv-comp__coach-num-box">
|
||||
{c === 'bar' ? (
|
||||
<BarGlassIcon size={20} color="#ffffff" />
|
||||
) : (
|
||||
<span>{c}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Espace motrice arrière */}
|
||||
<div className="aff-tgv-comp__coach-num-motrice" />
|
||||
</div>
|
||||
|
||||
{/* Silhouette du train TGV */}
|
||||
<div className="aff-tgv-comp__train-graphic">
|
||||
{/* Motrice avant arrondie */}
|
||||
<div className="aff-tgv-comp__loco-front" />
|
||||
|
||||
{/* Voitures 1 à 8 */}
|
||||
<div className="aff-tgv-comp__coaches-body">
|
||||
{coaches.map((c, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`aff-tgv-comp__coach-segment ${c === 'bar' ? 'aff-tgv-comp__coach-segment--bar' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Motrice arrière arrondie */}
|
||||
<div className="aff-tgv-comp__loco-rear" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TgvCompositionQuai({
|
||||
platform = '',
|
||||
destination = '',
|
||||
destination2 = '',
|
||||
marqueeMessage = '',
|
||||
composition = null,
|
||||
reperes = ['R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
|
||||
}) {
|
||||
const cleanTrack = String(platform || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Détermination des tranches (Rame 1 et Rame 2)
|
||||
const isMultiDest = Boolean(destination2 && destination2.trim() !== '' && destination2.toLowerCase() !== destination.toLowerCase());
|
||||
|
||||
const rame1Coaches = (composition && composition.rame1) || ['01', '02', '03', 'bar', '05', '06', '07', '08'];
|
||||
const rame2Coaches = (composition && composition.rame2) || ['11', '12', '13', 'bar', '15', '16', '17', '18'];
|
||||
|
||||
const defaultMarquee = marqueeMessage || 'MERCI DE VOTRE COMPRÉHENSION..........................................................................................';
|
||||
|
||||
return (
|
||||
<div className="aff-tgv-comp__container">
|
||||
{/* LIGNE DU HAUT : BOX VOIE + VISUALISATION DU TRAIN */}
|
||||
<div className="aff-tgv-comp__top-row">
|
||||
{/* Indicateur Voie à gauche */}
|
||||
<div className="aff-tgv-comp__track-box">
|
||||
<div className="aff-tgv-comp__track-pill">Voie</div>
|
||||
<div className="aff-tgv-comp__track-letter">{cleanTrack ? cleanTrack.toUpperCase() : '-'}</div>
|
||||
<div className="aff-tgv-comp__track-underline" />
|
||||
</div>
|
||||
|
||||
{/* Visualisation Composition des Rames */}
|
||||
<div className="aff-tgv-comp__rames-container">
|
||||
<TgvRameShape
|
||||
destination={destination}
|
||||
coaches={rame1Coaches}
|
||||
/>
|
||||
<TgvRameShape
|
||||
destination={isMultiDest ? destination2 : destination}
|
||||
coaches={rame2Coaches}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2) */}
|
||||
<div className="aff-tgv-comp__reperes-line-wrapper">
|
||||
<div className="aff-tgv-comp__dotted-line" />
|
||||
{reperes && reperes.length > 0 && (
|
||||
<div className="aff-tgv-comp__reperes-row">
|
||||
{reperes.map((rep, idx) => (
|
||||
<div key={idx} className="aff-tgv-comp__repere-box">
|
||||
{rep}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* LIGNE DU BAS : DÉFILEMENT MESSAGE + HORLOGE */}
|
||||
<div className="aff-tgv-comp__bottom-row">
|
||||
<div className="aff-tgv-comp__marquee-wrapper">
|
||||
<div className="aff-tgv-comp__marquee-text">
|
||||
{defaultMarquee}
|
||||
</div>
|
||||
</div>
|
||||
<div className="aff-tgv-comp__clock-wrapper">
|
||||
<TgvDigitalClock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
-1
@@ -1,6 +1,30 @@
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('./fonts/achemine_italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
font-family: 'Achemine', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
@@ -408,6 +408,73 @@ export default function SelectAfficheur({ onBack, navigate }) {
|
||||
🚉 Lancer l'Afficheur de Quai
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options 3 & 4 pour le mode Classiques : Flash Info & Informations */}
|
||||
{typeParam === 'classiques' && (
|
||||
<>
|
||||
{/* Option 3 : Flash Information */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">⚡</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#fab23e', color: '#00337f' }}>
|
||||
Flash Info Trafic
|
||||
</span>
|
||||
<h2 className="launcher-card-title">Flash Information</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Écran dédié aux perturbations de circulation en temps réel et travaux de maintenance sur le réseau.
|
||||
</p>
|
||||
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
|
||||
⚡ Conforme au design historique <em>Flash Circulation</em> avec fond ambre et bandeau bleu nuit.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/flash-info?gare=${encodeURIComponent(kebab)}`);
|
||||
}}
|
||||
className="launcher-btn"
|
||||
style={{ background: '#00337f', color: '#fab23e', border: 'none', fontWeight: 700 }}
|
||||
>
|
||||
⚡ Lancer le Flash Information
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Option 4 : Informations (Messages Dynamiques) */}
|
||||
<div className="launcher-select-card" style={{ cursor: 'default' }}>
|
||||
<div>
|
||||
<span className="launcher-card-icon">ℹ️</span>
|
||||
<span className="launcher-card-tag launcher-card-tag--retro" style={{ background: '#00337f', color: '#ffffff' }}>
|
||||
Messages Gare
|
||||
</span>
|
||||
<h2 className="launcher-card-title">Informations Voyageurs</h2>
|
||||
<p className="launcher-card-desc">
|
||||
Écran d'annonces et consignes dynamiques de la gare (Vigipirate, étiquetage des bagages, sécurité en gare).
|
||||
</p>
|
||||
<div style={{ background: '#f8fafc', border: '1px solid #e2ebf6', borderRadius: '10px', padding: '16px', marginBottom: '20px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#00337f', fontWeight: 600 }}>
|
||||
ℹ️ Conforme au design historique <em>Information</em> avec messages voyageurs de la gare de {decodedGare}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const kebab = gareParam ? String(gareParam).trim() : '';
|
||||
navigate(`${prefix}/afficheurs/classiques/informations?gare=${encodeURIComponent(kebab)}`);
|
||||
}}
|
||||
className="launcher-btn"
|
||||
style={{ background: '#00337f', color: '#ffffff', border: 'none', fontWeight: 700 }}
|
||||
>
|
||||
ℹ️ Lancer l'Afficheur Informations
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Accès direct aux pages fixes AFL */}
|
||||
|
||||
@@ -6,7 +6,8 @@ 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';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
@@ -219,12 +220,8 @@ export default function AFLArrivees(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -662,17 +659,18 @@ export default function AFLArrivees(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
|
||||
horaire._displayPlatform = String(platformVal).trim();
|
||||
let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
|
||||
useEffect(() => {
|
||||
@@ -699,11 +697,13 @@ export default function AFLArrivees(){
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
|
||||
const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(timeStr),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
|
||||
_displayPlatform: plat,
|
||||
_arrHM: arrHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -784,25 +784,24 @@ export default function AFLArrivees(){
|
||||
// comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare de départ (on veut les arrivées, pas les départs)
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare d'arrivée finale correspond
|
||||
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (arriveeName === normalized) return true;
|
||||
const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0)
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
|
||||
if (isOrigin) continue; // Si c'est l'origine, ça ne peut pas être une arrivée
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -868,7 +867,7 @@ export default function AFLArrivees(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1300,7 +1299,17 @@ export default function AFLArrivees(){
|
||||
).toString().trim();
|
||||
|
||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
const isCarTER = (
|
||||
op === 'CAR TER' ||
|
||||
op === 'AUTOCAR' ||
|
||||
op.includes('AUTOCAR') ||
|
||||
op.includes('CAR') ||
|
||||
op.includes('BUS') ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
isSub
|
||||
);
|
||||
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
||||
|
||||
@@ -6,6 +6,11 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import Logo from '../../../components/Logo';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
import AFLArrivees from './AFLArrivees';
|
||||
|
||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
@@ -28,12 +33,16 @@ const AFLArriveesQuais = () => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [nextArrival, setNextArrival] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const gareParam = searchParams.get('gare') || '';
|
||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
|
||||
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
|
||||
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
|
||||
|
||||
useAnnonceSonoreRealtime(null, 'AFLArriveesQuais', quaiParam);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -365,9 +374,20 @@ const AFLArriveesQuais = () => {
|
||||
quai: quaiParam
|
||||
});
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
const train = sncfData[0];
|
||||
let matching = sncfData;
|
||||
if (quaiParam) {
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
|
||||
}
|
||||
const train = matching[0];
|
||||
if (train) {
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const enriched = {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_rawTime: train.heure_arrivee || train.heure_depart,
|
||||
_displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
|
||||
_status: {
|
||||
@@ -384,6 +404,9 @@ const AFLArriveesQuais = () => {
|
||||
} else {
|
||||
setNextArrival(null);
|
||||
}
|
||||
} else {
|
||||
setNextArrival(null);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -583,25 +606,24 @@ const AFLArriveesQuais = () => {
|
||||
// Filter by station: keep arrivals to this station or stops at this station (excluding origin)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
// Exclude if target station is the departure origin
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Include if target station is the terminus
|
||||
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (arriveeName === normalized) return true;
|
||||
const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
|
||||
|
||||
// Check intermediate stops
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
|
||||
if (isOrigin) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -668,8 +690,10 @@ const AFLArriveesQuais = () => {
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
return quaiParam && platform.toLowerCase() === quaiParam.toLowerCase();
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (!quaiParam) return true;
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
return cleanP(platform) === cleanP(quaiParam);
|
||||
});
|
||||
|
||||
if (candidatesOnPlatform.length > 0) {
|
||||
@@ -684,13 +708,44 @@ const AFLArriveesQuais = () => {
|
||||
}
|
||||
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
|
||||
|
||||
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
|
||||
useEffect(() => {
|
||||
if (nextArrival) return;
|
||||
let duration = 20000;
|
||||
if (idlePhase === 2) duration = 10000;
|
||||
else duration = 20000;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIdlePhase(prev => (prev + 1) % 3);
|
||||
}, duration);
|
||||
return () => clearTimeout(timer);
|
||||
}, [idlePhase, nextArrival]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 15000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchData]);
|
||||
|
||||
if (loading) return <div className="afl-q-arr-container" style={{ background: '#173e31' }}></div>;
|
||||
// AFFICHAGE 1 : Page Vidéoprotection (via query)
|
||||
if (isVideoprotection) {
|
||||
return <GareVideoprotection />;
|
||||
}
|
||||
|
||||
// AFFICHAGE 2 : Alternance si aucun train n'est attribué à ce quai
|
||||
if (!nextArrival && gareParam) {
|
||||
if (idlePhase === 0) {
|
||||
return <AttentifsEnsemble />;
|
||||
}
|
||||
if (idlePhase === 1) {
|
||||
return <AFLArrivees />;
|
||||
}
|
||||
if (idlePhase === 2) {
|
||||
return <AFLInformationsPage mode="arrivees" gareName={gareDisplay || 'Votre Gare'} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !nextArrival) return <div className="afl-q-arr-container" style={{ background: '#173e31' }}></div>;
|
||||
|
||||
if (!nextArrival) {
|
||||
return (
|
||||
@@ -821,10 +876,9 @@ const AFLArriveesQuais = () => {
|
||||
|
||||
let journeyStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
const normCurrent = normalizeName(currentStationName);
|
||||
const endIdx = stopsList.findIndex(s => {
|
||||
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
|
||||
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
|
||||
const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
|
||||
return sName && matchStationNameStrict(sName, currentStationName);
|
||||
});
|
||||
|
||||
const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice();
|
||||
|
||||
@@ -6,7 +6,8 @@ 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';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
@@ -227,12 +228,8 @@ export default function AFLDeparts(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -670,17 +667,18 @@ export default function AFLDeparts(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
|
||||
horaire._displayPlatform = String(platformVal).trim();
|
||||
let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatus(s => !s), 3000);
|
||||
@@ -710,18 +708,22 @@ export default function AFLDeparts(){
|
||||
if (sncfErr) {
|
||||
setError(sncfErr);
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => ({
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(t.heure_depart),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
|
||||
_displayPlatform: plat,
|
||||
_depHM: depHM,
|
||||
_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);
|
||||
setAllHoraires(formatted);
|
||||
}
|
||||
@@ -795,15 +797,14 @@ export default function AFLDeparts(){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -816,11 +817,11 @@ export default function AFLDeparts(){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -893,7 +894,7 @@ export default function AFLDeparts(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1325,7 +1326,17 @@ export default function AFLDeparts(){
|
||||
).toString().trim();
|
||||
|
||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
const isCarTER = (
|
||||
op === 'CAR TER' ||
|
||||
op === 'AUTOCAR' ||
|
||||
op.includes('AUTOCAR') ||
|
||||
op.includes('CAR') ||
|
||||
op.includes('BUS') ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
isSub
|
||||
);
|
||||
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
|
||||
@@ -6,6 +6,12 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import Logo from '../../../components/Logo';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
|
||||
import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
import AFLDeparts from './AFLDeparts';
|
||||
|
||||
// Helper utilitaire : format HH:MM sans secondes (ex: 15:57)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
@@ -28,6 +34,7 @@ const AFLDepartsQuais = () => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [nextDeparture, setNextDeparture] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
|
||||
|
||||
// Parse URL manually since we are not in a React Router context
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
@@ -36,6 +43,10 @@ const AFLDepartsQuais = () => {
|
||||
const quaiParam = searchParams.get('quai') || searchParams.get('voie') || '';
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
|
||||
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
|
||||
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
|
||||
const isForceDepartEnCours = pageParam === 'depart_en_cours' || searchParams.get('depart_en_cours') === 'true' || searchParams.get('test_depart') === 'true';
|
||||
|
||||
const [resolvedGareId, setResolvedGareId] = useState(gareIdParam || null);
|
||||
useAnnonceSonoreRealtime(resolvedGareId, 'AFLDepartsQuais', quaiParam);
|
||||
|
||||
@@ -435,9 +446,20 @@ const AFLDepartsQuais = () => {
|
||||
quai: quaiParam
|
||||
});
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
const train = sncfData[0];
|
||||
let matching = sncfData;
|
||||
if (quaiParam) {
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
|
||||
}
|
||||
const train = matching[0];
|
||||
if (train) {
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const enriched = {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_rawTime: train.heure_depart,
|
||||
_displayTime: formatTimeNoSeconds(train.heure_depart),
|
||||
_status: {
|
||||
@@ -454,6 +476,9 @@ const AFLDepartsQuais = () => {
|
||||
} else {
|
||||
setNextDeparture(null);
|
||||
}
|
||||
} else {
|
||||
setNextDeparture(null);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -653,13 +678,12 @@ const AFLDepartsQuais = () => {
|
||||
// Filter by station: keep departures from this station or stops at this station (excluding terminus)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
@@ -671,11 +695,11 @@ const AFLDepartsQuais = () => {
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -741,9 +765,10 @@ const AFLDepartsQuais = () => {
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (!quaiParam) return true;
|
||||
return platform.toLowerCase() === quaiParam.toLowerCase();
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
return cleanP(platform) === cleanP(quaiParam);
|
||||
});
|
||||
|
||||
if (candidatesOnPlatform.length > 0) {
|
||||
@@ -758,13 +783,66 @@ const AFLDepartsQuais = () => {
|
||||
}
|
||||
}, [applyPerturbationsToHoraires, buildPerturbMap, gareDisplay, gareParam, getPerturbationJours, getStops, normalizeName, parseTimeHM, quaiParam]);
|
||||
|
||||
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
|
||||
useEffect(() => {
|
||||
if (nextDeparture) return;
|
||||
let duration = 20000;
|
||||
if (idlePhase === 2) duration = 10000;
|
||||
else duration = 20000;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIdlePhase(prev => (prev + 1) % 3);
|
||||
}, duration);
|
||||
return () => clearTimeout(timer);
|
||||
}, [idlePhase, nextDeparture]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 15000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchData]);
|
||||
|
||||
if (loading) return <div className="afl-q-container" style={{ background: '#0c0f2e' }}></div>;
|
||||
// 1. Détection "Départ en cours" (2 minutes avant l'heure de départ réelle)
|
||||
let isDepartEnCours = isForceDepartEnCours;
|
||||
if (!isDepartEnCours && nextDeparture) {
|
||||
const rawTime = nextDeparture._rawTime || nextDeparture.heure_depart || nextDeparture.time;
|
||||
const depHM = parseTimeHM(rawTime);
|
||||
const delayMin = nextDeparture._status?.delayMinutes || nextDeparture.retard_minutes || 0;
|
||||
if (depHM != null) {
|
||||
const nowSec = currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds();
|
||||
const depSec = (depHM + delayMin) * 60;
|
||||
let diffSec = depSec - nowSec;
|
||||
if (diffSec < -43200) diffSec += 86400;
|
||||
if (diffSec <= 120 && diffSec >= 0) {
|
||||
isDepartEnCours = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AFFICHAGE 1 : Page Vidéoprotection (via query)
|
||||
if (isVideoprotection) {
|
||||
return <GareVideoprotection />;
|
||||
}
|
||||
|
||||
// AFFICHAGE 2 : Départ en cours (T - 2 min)
|
||||
if (isDepartEnCours) {
|
||||
return <DepartEnCoursAFL />;
|
||||
}
|
||||
|
||||
// AFFICHAGE 3 : Alternance si aucun train n'est attribué à ce quai
|
||||
if (!nextDeparture && gareParam) {
|
||||
if (idlePhase === 0) {
|
||||
return <AttentifsEnsemble />;
|
||||
}
|
||||
if (idlePhase === 1) {
|
||||
return <AFLDeparts />;
|
||||
}
|
||||
if (idlePhase === 2) {
|
||||
return <AFLInformationsPage mode="departs" gareName={gareDisplay || 'Votre Gare'} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !nextDeparture) return <div className="afl-q-container" style={{ background: '#0c0f2e' }}></div>;
|
||||
|
||||
if (!nextDeparture) {
|
||||
return (
|
||||
@@ -900,10 +978,9 @@ const AFLDepartsQuais = () => {
|
||||
|
||||
let downstreamStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
const normCurrent = normalizeName(currentStationName);
|
||||
const startIdx = stopsList.findIndex(s => {
|
||||
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
|
||||
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
|
||||
const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
|
||||
return sName && matchStationNameStrict(sName, currentStationName);
|
||||
});
|
||||
|
||||
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
|
||||
@@ -912,7 +989,7 @@ const AFLDepartsQuais = () => {
|
||||
rawDownstream.forEach(s => {
|
||||
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
|
||||
const norm = normalizeName(name);
|
||||
if (name && norm !== normCurrent && !seen.has(norm)) {
|
||||
if (name && !matchStationNameStrict(name, currentStationName) && !seen.has(norm)) {
|
||||
seen.add(norm);
|
||||
downstreamStops.push(name);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
.aff-classiques-arrivees__stop-bullet {
|
||||
color: var(--yellow);
|
||||
font-size: 50px;
|
||||
font-size: 46px;
|
||||
margin-right: 14px;
|
||||
margin-left: 18px;
|
||||
line-height: 1;
|
||||
@@ -174,12 +174,12 @@
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__stop-name {
|
||||
color: var(--white);
|
||||
font-size: 58px;
|
||||
color: #d0d8e2;
|
||||
font-size: 62px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
font-family: "Achemine", sans-serif;
|
||||
letter-spacing: -0.5px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
@@ -203,8 +203,8 @@
|
||||
|
||||
/* Colonne 2 : Type de train + Numéro OU Statut */
|
||||
.aff-classiques-arrivees__col-meta {
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -217,53 +217,72 @@
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__train-type {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__train-number {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-ontime {
|
||||
color: var(--white);
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed {
|
||||
color: var(--yellow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed-label {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed-time {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-suppressed {
|
||||
color: #ff4d4d;
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-arrivees__col-time {
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -276,7 +295,7 @@
|
||||
|
||||
.aff-classiques-arrivees__time {
|
||||
color: var(--yellow);
|
||||
font-size: 68px;
|
||||
font-size: 76px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
@@ -302,13 +321,14 @@
|
||||
|
||||
.aff-classiques-arrivees__destination {
|
||||
color: var(--white);
|
||||
font-size: 62px;
|
||||
font-weight: 600;
|
||||
font-size: 70px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "Achemine", sans-serif;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
|
||||
/* Colonne 5 : Quai / Voie (Premier plan absolu z-index: 20, au-dessus du watermark) */
|
||||
@@ -325,10 +345,10 @@
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__platform-box {
|
||||
width: 88px;
|
||||
height: 84px;
|
||||
border: 3.5px solid var(--white);
|
||||
border-radius: 12px;
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
border: 3px solid var(--white);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -356,8 +376,8 @@
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding-left: 24px;
|
||||
padding-right: 12px;
|
||||
background: linear-gradient(180deg, #13571d 0%, #0a2d0f 100%) !important;
|
||||
padding-right: 14px;
|
||||
background: linear-gradient(180deg, #165b21 0%, #0a2d0f 100%) !important;
|
||||
position: relative;
|
||||
z-index: 25;
|
||||
border-top: 1.5px solid rgba(255, 255, 255, 0.18);
|
||||
@@ -386,23 +406,22 @@
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.85);
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 14px 4px 14px;
|
||||
background: rgba(10, 42, 16, 0.3);
|
||||
background: rgba(10, 42, 16, 0.45);
|
||||
font-family: "Achemine", sans-serif;
|
||||
color: var(--white);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
box-sizing: border-box;
|
||||
height: 76px;
|
||||
height: 74px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__bottom-clock-hm {
|
||||
font-size: 54px;
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 2px;
|
||||
margin-right: 8px;
|
||||
letter-spacing: 1.5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -410,7 +429,8 @@
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: 1px;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--yellow) !important;
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
@@ -6,9 +6,23 @@ 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';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
|
||||
|
||||
// Pictogramme Autocar SNCF / MOBIGO
|
||||
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
style={{ flexShrink: 0, ...style }}
|
||||
>
|
||||
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
@@ -143,7 +157,7 @@ function Clock(){
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesArrivees(){
|
||||
export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
// lire le paramètre de requête ?gare=Nom-de-la-gare
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const gareParam = params.get('gare');
|
||||
@@ -151,8 +165,8 @@ export default function ClassiquesArrivees(){
|
||||
const gareIdParam = params.get('gare_id') || params.get('gareId');
|
||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
|
||||
const defilementMode = params.get('defilement') === 'true';
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL ou passé en prop
|
||||
const defilementMode = defilement !== null ? Boolean(defilement) : (params.get('defilement') === 'true');
|
||||
|
||||
// debug flag activable via ?debug=1 or ?debug=true
|
||||
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
||||
@@ -212,12 +226,8 @@ export default function ClassiquesArrivees(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -650,21 +660,20 @@ export default function ClassiquesArrivees(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
// Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
@@ -707,11 +716,13 @@ export default function ClassiquesArrivees(){
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
|
||||
const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(timeStr),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
|
||||
_displayPlatform: plat,
|
||||
_arrHM: arrHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -720,8 +731,10 @@ export default function ClassiquesArrivees(){
|
||||
}
|
||||
};
|
||||
});
|
||||
setHoraires(formatted);
|
||||
setAllHoraires(formatted);
|
||||
const maxCount = defilementMode ? 20 : 10;
|
||||
const limited = (formatted || []).slice(0, maxCount);
|
||||
setHoraires(limited);
|
||||
setAllHoraires(limited);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setError(e.message || String(e));
|
||||
@@ -791,15 +804,14 @@ export default function ClassiquesArrivees(){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare de départ
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare d'arrivée correspond
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return true;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le point de départ (isFirst ou index==0)
|
||||
const stopsList = getStops(h);
|
||||
@@ -812,11 +824,11 @@ export default function ClassiquesArrivees(){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
|
||||
if (isOrigine) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -889,7 +901,7 @@ export default function ClassiquesArrivees(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1165,10 +1177,13 @@ export default function ClassiquesArrivees(){
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
const maxRowsCount = defilementMode ? 20 : 10;
|
||||
const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
|
||||
: limitedHoraires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
@@ -1259,7 +1274,21 @@ export default function ClassiquesArrivees(){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
const isCar = (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
Boolean(horaire.is_substitution) ||
|
||||
(numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
|
||||
typeLabel === 'AUTOCAR' ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
cleanPlatform === 'AUTOCAR' ||
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForArrivees(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
|
||||
@@ -1280,7 +1309,10 @@ export default function ClassiquesArrivees(){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
<div className="aff-classiques-arrivees__status-delayed">
|
||||
<div className="aff-classiques-arrivees__status-delayed-label">retardé</div>
|
||||
<div className="aff-classiques-arrivees__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
|
||||
)
|
||||
@@ -1293,7 +1325,12 @@ export default function ClassiquesArrivees(){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-time">
|
||||
<div className="aff-classiques-arrivees__time">{time}</div>
|
||||
<div
|
||||
className="aff-classiques-arrivees__time"
|
||||
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-destination">
|
||||
@@ -1303,7 +1340,11 @@ export default function ClassiquesArrivees(){
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-arrivees__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1314,11 +1355,12 @@ export default function ClassiquesArrivees(){
|
||||
<div className="aff-classiques-arrivees__sub-stops">
|
||||
<MarqueeText
|
||||
className="aff-classiques-arrivees__sub-stops-marquee"
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-arrivees__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-arrivees__stop-item">
|
||||
<span className="aff-classiques-arrivees__stop-bullet">•</span>
|
||||
{sIdx > 0 && <span className="aff-classiques-arrivees__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-arrivees__stop-name">{stopName}</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -1345,7 +1387,10 @@ export default function ClassiquesArrivees(){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
<div className="aff-classiques-arrivees__status-delayed">
|
||||
<div className="aff-classiques-arrivees__status-delayed-label">retardé</div>
|
||||
<div className="aff-classiques-arrivees__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
|
||||
)
|
||||
@@ -1359,7 +1404,12 @@ export default function ClassiquesArrivees(){
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<div className="aff-classiques-arrivees__col-time">
|
||||
<div className="aff-classiques-arrivees__time">{time}</div>
|
||||
<div
|
||||
className="aff-classiques-arrivees__time"
|
||||
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Provenance */}
|
||||
@@ -1371,7 +1421,11 @@ export default function ClassiquesArrivees(){
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-arrivees__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
.aff-classiques-departs__stop-bullet {
|
||||
color: var(--yellow);
|
||||
font-size: 50px;
|
||||
font-size: 46px;
|
||||
margin-right: 14px;
|
||||
margin-left: 18px;
|
||||
line-height: 1;
|
||||
@@ -174,12 +174,12 @@
|
||||
}
|
||||
|
||||
.aff-classiques-departs__stop-name {
|
||||
color: var(--white);
|
||||
font-size: 58px;
|
||||
color: #d0d8e2;
|
||||
font-size: 62px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
font-family: "Achemine", sans-serif;
|
||||
letter-spacing: -0.5px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
/* Colonne 1 : Logo opérateur (blanc) */
|
||||
@@ -203,8 +203,8 @@
|
||||
|
||||
/* Colonne 2 : Type de train + Numéro OU Statut */
|
||||
.aff-classiques-departs__col-meta {
|
||||
width: 185px;
|
||||
min-width: 185px;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -217,53 +217,72 @@
|
||||
}
|
||||
|
||||
.aff-classiques-departs__train-type {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__train-number {
|
||||
font-size: 26px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--white);
|
||||
line-height: 1.1;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-ontime {
|
||||
color: var(--white);
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed {
|
||||
color: var(--yellow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed-label {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed-time {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-suppressed {
|
||||
color: #ff4d4d;
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
font-family: "Achemine", sans-serif;
|
||||
}
|
||||
|
||||
/* Colonne 3 : Heure (Grande, jaune vif) */
|
||||
.aff-classiques-departs__col-time {
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -276,7 +295,7 @@
|
||||
|
||||
.aff-classiques-departs__time {
|
||||
color: var(--yellow);
|
||||
font-size: 68px;
|
||||
font-size: 76px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
@@ -302,13 +321,14 @@
|
||||
|
||||
.aff-classiques-departs__destination {
|
||||
color: var(--white);
|
||||
font-size: 62px;
|
||||
font-weight: 600;
|
||||
font-size: 70px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "Achemine", sans-serif;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
|
||||
/* Colonne 5 : Quai / Voie (Premier plan absolu z-index: 20, au-dessus du watermark) */
|
||||
@@ -325,10 +345,10 @@
|
||||
}
|
||||
|
||||
.aff-classiques-departs__platform-box {
|
||||
width: 88px;
|
||||
height: 84px;
|
||||
border: 3.5px solid var(--white);
|
||||
border-radius: 12px;
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
border: 3px solid var(--white);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -356,8 +376,8 @@
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding-left: 24px;
|
||||
padding-right: 12px;
|
||||
background: linear-gradient(180deg, #07418a 0%, #00225e 100%) !important;
|
||||
padding-right: 14px;
|
||||
background: linear-gradient(180deg, #093c83 0%, #001f55 100%) !important;
|
||||
position: relative;
|
||||
z-index: 25;
|
||||
border-top: 1.5px solid rgba(255, 255, 255, 0.18);
|
||||
@@ -386,23 +406,22 @@
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
border: 2.5px solid rgba(255, 255, 255, 0.85);
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 14px 4px 14px;
|
||||
background: rgba(0, 20, 60, 0.3);
|
||||
background: rgba(0, 24, 64, 0.45);
|
||||
font-family: "Achemine", sans-serif;
|
||||
color: var(--white);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
box-sizing: border-box;
|
||||
height: 76px;
|
||||
height: 74px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__bottom-clock-hm {
|
||||
font-size: 54px;
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 2px;
|
||||
margin-right: 8px;
|
||||
letter-spacing: 1.5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -410,7 +429,8 @@
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: 1px;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--yellow) !important;
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
@@ -6,9 +6,23 @@ 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';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
|
||||
|
||||
// Pictogramme Autocar SNCF / MOBIGO
|
||||
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
style={{ flexShrink: 0, ...style }}
|
||||
>
|
||||
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
@@ -143,7 +157,7 @@ function Clock(){
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesDeparts(){
|
||||
export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
// lire le paramètre de requête ?gare=Nom-de-la-gare
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const gareParam = params.get('gare');
|
||||
@@ -151,8 +165,8 @@ export default function ClassiquesDeparts(){
|
||||
const gareIdParam = params.get('gare_id') || params.get('gareId');
|
||||
// gérer les +, décoder et remplacer les tirets par des espaces
|
||||
const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null;
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL
|
||||
const defilementMode = params.get('defilement') === 'true';
|
||||
// Mode défilement (marquee vertical infini) si ?defilement=true dans l'URL ou passé en prop
|
||||
const defilementMode = defilement !== null ? Boolean(defilement) : (params.get('defilement') === 'true');
|
||||
|
||||
// debug flag activable via ?debug=1 or ?debug=true
|
||||
const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true');
|
||||
@@ -212,12 +226,8 @@ export default function ClassiquesDeparts(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -650,21 +660,20 @@ export default function ClassiquesDeparts(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
// Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
@@ -705,20 +714,26 @@ export default function ClassiquesDeparts(){
|
||||
if (sncfErr) {
|
||||
setError(sncfErr);
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => ({
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(t.heure_depart),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
|
||||
_displayPlatform: plat,
|
||||
_depHM: depHM,
|
||||
_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);
|
||||
setAllHoraires(formatted);
|
||||
};
|
||||
});
|
||||
const maxCount = defilementMode ? 20 : 10;
|
||||
const limited = (formatted || []).slice(0, maxCount);
|
||||
setHoraires(limited);
|
||||
setAllHoraires(limited);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setError(e.message || String(e));
|
||||
@@ -788,15 +803,14 @@ export default function ClassiquesDeparts(){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -809,11 +823,11 @@ export default function ClassiquesDeparts(){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -886,7 +900,7 @@ export default function ClassiquesDeparts(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1162,10 +1176,13 @@ export default function ClassiquesDeparts(){
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
const maxRowsCount = defilementMode ? 20 : 10;
|
||||
const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
|
||||
: limitedHoraires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
@@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts(){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
const isCar = (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
Boolean(horaire.is_substitution) ||
|
||||
(numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
|
||||
typeLabel === 'AUTOCAR' ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
cleanPlatform === 'AUTOCAR' ||
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
|
||||
@@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts(){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
<div className="aff-classiques-departs__status-delayed">
|
||||
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
|
||||
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
@@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts(){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
<div
|
||||
className="aff-classiques-departs__time"
|
||||
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts(){
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1311,11 +1354,12 @@ export default function ClassiquesDeparts(){
|
||||
<div className="aff-classiques-departs__sub-stops">
|
||||
<MarqueeText
|
||||
className="aff-classiques-departs__sub-stops-marquee"
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-departs__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-departs__stop-item">
|
||||
<span className="aff-classiques-departs__stop-bullet">•</span>
|
||||
{sIdx > 0 && <span className="aff-classiques-departs__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-departs__stop-name">{stopName}</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -1342,7 +1386,10 @@ export default function ClassiquesDeparts(){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : isDelayed ? (
|
||||
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div>
|
||||
<div className="aff-classiques-departs__status-delayed">
|
||||
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
|
||||
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classiques-departs__status-ontime">à l'heure</div>
|
||||
)
|
||||
@@ -1356,7 +1403,12 @@ export default function ClassiquesDeparts(){
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<div className="aff-classiques-departs__col-time">
|
||||
<div className="aff-classiques-departs__time">{time}</div>
|
||||
<div
|
||||
className="aff-classiques-departs__time"
|
||||
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COL 4 : Destination */}
|
||||
@@ -1368,7 +1420,11 @@ export default function ClassiquesDeparts(){
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fab23e;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
CONTAINER (1920x1080)
|
||||
========================================================= */
|
||||
.aff-classique-flash__container {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #fab23e;
|
||||
color: #00337f;
|
||||
font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
SIDEBAR GAUCHE : BLEU NUIT (Conforme Image 3)
|
||||
========================================================= */
|
||||
.aff-classique-flash__sidebar {
|
||||
width: 320px;
|
||||
height: 1080px;
|
||||
background-color: #00337f;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aff-classique-flash__sidebar-text {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center center;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 24px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.aff-classique-flash__title-flash {
|
||||
color: #f5a623;
|
||||
font-size: 10.5rem; /* ~125px */
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-classique-flash__title-circulation {
|
||||
color: #ffffff;
|
||||
font-size: 10.5rem; /* ~125px */
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
ZONE PRINCIPALE : JAUNE / AMBRE (Conforme Image 3)
|
||||
========================================================= */
|
||||
.aff-classique-flash__main {
|
||||
flex: 1;
|
||||
height: 1080px;
|
||||
background-color: #fab23e;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 60px 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-classique-flash__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
/* AFFICHAGE NOMINAL (TRAFIC NORMAL) */
|
||||
.aff-classique-flash__nominal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
max-width: 1300px;
|
||||
}
|
||||
|
||||
.aff-classique-flash__nominal-title {
|
||||
color: #00337f;
|
||||
font-size: 6.5rem; /* ~78px */
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.1;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.aff-classique-flash__nominal-subtitle {
|
||||
color: #00337f;
|
||||
font-size: 3.8rem; /* ~45px */
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.aff-classique-flash__nominal-detail {
|
||||
color: #00337f;
|
||||
font-size: 2.8rem; /* ~33px */
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* CARTES D'ALERTE TRAFIC */
|
||||
.aff-classique-flash__alerts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
max-width: 1350px;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-card {
|
||||
background-color: #00337f;
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 32px 40px;
|
||||
box-shadow: 0 8px 24px rgba(0, 51, 127, 0.2);
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-card--travaux {
|
||||
background-color: #1a2332;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-tag {
|
||||
background-color: #d32f2f;
|
||||
color: #ffffff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
padding: 4px 14px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-tag--travaux {
|
||||
background-color: #f5a623;
|
||||
color: #00337f;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-line {
|
||||
font-size: 2.6rem;
|
||||
font-weight: 700;
|
||||
color: #fab23e;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-title {
|
||||
font-size: 3.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.aff-classique-flash__alert-desc {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
color: #e5eaf3;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
HORLOGE BAS DROITE (Conforme Image 3)
|
||||
========================================================= */
|
||||
.aff-classique-flash__footer-clock-box {
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
right: 35px;
|
||||
background-color: #00337f;
|
||||
border: 2.5px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 4px 16px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.aff-classique-flash__clock {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-classique-flash__clock-hm {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-classique-flash__clock-s {
|
||||
color: #f5a623;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './ClassiquesFlashInfo.css';
|
||||
import useAfficheurData from '../../../hooks/useAfficheurData';
|
||||
|
||||
function DigitalClock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
return (
|
||||
<div className="aff-classique-flash__clock">
|
||||
<span className="aff-classique-flash__clock-hm">{hh}:{mm}</span>
|
||||
<span className="aff-classique-flash__clock-s">{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesFlashInfo() {
|
||||
const params = new URLSearchParams(window.location.search || '');
|
||||
const gareParam = params.get('gare');
|
||||
const gareIdParam = params.get('gare_id') || params.get('gareId');
|
||||
|
||||
const gareDisplay = gareParam
|
||||
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
||||
: 'Votre Gare';
|
||||
|
||||
const {
|
||||
perturbations = [],
|
||||
travaux = []
|
||||
} = useAfficheurData({
|
||||
gareName: gareDisplay,
|
||||
gareId: gareIdParam,
|
||||
mode: 'departs',
|
||||
refreshIntervalMs: 20000
|
||||
});
|
||||
|
||||
const activePerturbations = (perturbations || []).filter(p => {
|
||||
const t = (p.type_perturbation || p.type || '').toString().toLowerCase();
|
||||
return Boolean(t);
|
||||
});
|
||||
|
||||
const hasTrafficAlerts = activePerturbations.length > 0 || (travaux && travaux.length > 0);
|
||||
|
||||
return (
|
||||
<div className="aff-classique-flash__container">
|
||||
{/* BANDEAU GAUCHE : BLEU NUIT (Conforme Image 3) */}
|
||||
<div className="aff-classique-flash__sidebar">
|
||||
<div className="aff-classique-flash__sidebar-text">
|
||||
<span className="aff-classique-flash__title-flash">flash</span>
|
||||
<span className="aff-classique-flash__title-circulation">circulation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZONE PRINCIPALE : AMBRE / JAUNE ORANGE (Conforme Image 3) */}
|
||||
<div className="aff-classique-flash__main">
|
||||
<div className="aff-classique-flash__content">
|
||||
{hasTrafficAlerts ? (
|
||||
<div className="aff-classique-flash__alerts-container">
|
||||
{activePerturbations.map((p, idx) => (
|
||||
<div key={`p-${idx}`} className="aff-classique-flash__alert-card">
|
||||
<div className="aff-classique-flash__alert-header">
|
||||
<span className="aff-classique-flash__alert-tag">Info Trafic</span>
|
||||
<span className="aff-classique-flash__alert-line">{p.ligne_nom || p.ligne || gareDisplay}</span>
|
||||
</div>
|
||||
<div className="aff-classique-flash__alert-title">
|
||||
{p.type_perturbation || p.titre || 'Perturbation en cours'}
|
||||
</div>
|
||||
<div className="aff-classique-flash__alert-desc">
|
||||
{p.message || p.description || p.detail || p.cause || 'Des adaptations de circulation peuvent survenir sur vos trajets. Veuillez écouter les annonces en gare.'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(travaux || []).slice(0, 2).map((t, idx) => (
|
||||
<div key={`t-${idx}`} className="aff-classique-flash__alert-card aff-classique-flash__alert-card--travaux">
|
||||
<div className="aff-classique-flash__alert-header">
|
||||
<span className="aff-classique-flash__alert-tag aff-classique-flash__alert-tag--travaux">Travaux</span>
|
||||
<span className="aff-classique-flash__alert-line">{t.nom || t.intitule || 'Maintenance du réseau'}</span>
|
||||
</div>
|
||||
<div className="aff-classique-flash__alert-title">
|
||||
{t.intitule || t.nom || 'Travaux de modernisation'}
|
||||
</div>
|
||||
<div className="aff-classique-flash__alert-desc">
|
||||
{t.description || t.motif || 'Des substitutions par autocars peuvent être mises en place. Merci de vérifier vos horaires.'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="aff-classique-flash__nominal">
|
||||
<div className="aff-classique-flash__nominal-box">
|
||||
<div className="aff-classique-flash__nominal-title">
|
||||
Trafic normal
|
||||
</div>
|
||||
<div className="aff-classique-flash__nominal-subtitle">
|
||||
La circulation des trains s'effectue normalement sur l'ensemble des lignes desservant la gare de {gareDisplay}.
|
||||
</div>
|
||||
<div className="aff-classique-flash__nominal-detail">
|
||||
Pour toute information complémentaire concernant vos trajets, nos agents en gare sont à votre disposition.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* HORLOGE EN BAS À DROITE (Conforme Image 3) */}
|
||||
<div className="aff-classique-flash__footer-clock-box">
|
||||
<DigitalClock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fab23e;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
CONTAINER (1920x1080)
|
||||
========================================================= */
|
||||
.aff-classique-infos__container {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #fab23e;
|
||||
color: #00337f;
|
||||
font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
SIDEBAR GAUCHE : BLEU NUIT (Conforme Image 4)
|
||||
========================================================= */
|
||||
.aff-classique-infos__sidebar {
|
||||
width: 320px;
|
||||
height: 1080px;
|
||||
background-color: #00337f;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aff-classique-infos__sidebar-text {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: center center;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.aff-classique-infos__title-information {
|
||||
color: #ffffff;
|
||||
font-size: 10.5rem; /* ~125px */
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
ZONE PRINCIPALE : CADRE JAUNE DE DROITE
|
||||
========================================================= */
|
||||
.aff-classique-infos__main {
|
||||
flex: 1;
|
||||
height: 1080px;
|
||||
background-color: #fab23e;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 80px 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* CONTENEUR DU MESSAGE UNIQUE (TEXTE BLEU SUR FOND JAUNE) */
|
||||
.aff-classique-infos__single-message-container {
|
||||
max-width: 1350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 36px;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.aff-classique-infos__single-title {
|
||||
color: #00337f;
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.aff-classique-infos__single-body {
|
||||
color: #00337f;
|
||||
font-size: 46px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
max-width: 1250px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
HORLOGE DIGITALE EN BAS À DROITE (Conforme Image 4)
|
||||
========================================================= */
|
||||
.aff-classique-infos__footer-clock-box {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aff-classique-infos__clock {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
background-color: #00337f;
|
||||
border: 4px solid #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 22px;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.aff-classique-infos__clock-hm {
|
||||
font-size: 3.4rem; /* ~54px */
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.aff-classique-infos__clock-s {
|
||||
font-size: 2.2rem; /* ~35px */
|
||||
font-weight: 800;
|
||||
color: #ff9e1b;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './ClassiquesInformations.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
|
||||
function DigitalClock() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const hh = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
return (
|
||||
<div className="aff-classique-infos__clock">
|
||||
<span className="aff-classique-infos__clock-hm">{hh}:{mm}</span>
|
||||
<span className="aff-classique-infos__clock-s">{ss}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClassiquesInformations() {
|
||||
const params = new URLSearchParams(window.location.search || '');
|
||||
const gareParam = params.get('gare');
|
||||
|
||||
const gareDisplay = gareParam
|
||||
? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ')
|
||||
: 'Votre Gare';
|
||||
|
||||
const defaultMessages = [
|
||||
{
|
||||
title: 'Plan Vigipirate & Sécurité',
|
||||
content: 'Ne laissez aucun bagage sans surveillance. Tout bagage abandonné sera détruit. Signalez tout colis ou comportement suspect aux agents SNCF ou aux forces de l\'ordre.'
|
||||
},
|
||||
{
|
||||
title: 'Étiquetage obligatoire de vos bagages',
|
||||
content: 'Pour votre tranquillité et la sécurité de tous, vos valises et sacs doivent impérativement comporter une étiquette visible indiquant vos nom, prénom, téléphone et adresse.'
|
||||
},
|
||||
{
|
||||
title: 'Traversée des voies formellement interdite',
|
||||
content: 'Pour votre sécurité, il est strictement interdit de descendre sur les voies ou de les traverser. Utilisez systématiquement les passages souterrains ou les passerelles.'
|
||||
},
|
||||
{
|
||||
title: 'Espace sans tabac ni vapotage',
|
||||
content: 'Conformément à la réglementation, il est strictement interdit de fumer ou de vapoter dans les espaces couverts, les bâtiments de la gare ainsi que sur les quais.'
|
||||
}
|
||||
];
|
||||
|
||||
const [messagesList, setMessagesList] = useState(defaultMessages);
|
||||
const [activeMessageIndex, setActiveMessageIndex] = useState(0);
|
||||
|
||||
// Chargement éventuel de messages dynamiques depuis Supabase
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const fetchStationMessages = async () => {
|
||||
if (!supabase) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('messages_dynamiques')
|
||||
.select('*')
|
||||
.order('id', { ascending: false });
|
||||
if (!error && Array.isArray(data) && data.length > 0 && mounted) {
|
||||
const formatted = data.map(m => ({
|
||||
title: m.titre || m.title || 'Information Voyageurs',
|
||||
content: m.contenu || m.content || m.message || ''
|
||||
})).filter(m => Boolean(m.content));
|
||||
if (formatted.length > 0) {
|
||||
setMessagesList(formatted);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Notice chargement messages_dynamiques:', e);
|
||||
}
|
||||
};
|
||||
fetchStationMessages();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
// Défilement d'un seul message à la fois toutes les 10s si plusieurs messages
|
||||
useEffect(() => {
|
||||
if (messagesList.length <= 1) return;
|
||||
const timer = setInterval(() => {
|
||||
setActiveMessageIndex(prev => (prev + 1) % messagesList.length);
|
||||
}, 10000);
|
||||
return () => clearInterval(timer);
|
||||
}, [messagesList.length]);
|
||||
|
||||
const activeMsg = messagesList[activeMessageIndex % messagesList.length] || defaultMessages[0];
|
||||
|
||||
return (
|
||||
<div className="aff-classique-infos__container">
|
||||
{/* BANDEAU GAUCHE : BLEU NUIT (Conforme Image 4) */}
|
||||
<div className="aff-classique-infos__sidebar">
|
||||
<div className="aff-classique-infos__sidebar-text">
|
||||
<span className="aff-classique-infos__title-information">information</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZONE PRINCIPALE : CADRE JAUNE DE DROITE AVEC TEXTE BLEU (Conforme Image 4) */}
|
||||
<div className="aff-classique-infos__main">
|
||||
<div className="aff-classique-infos__single-message-container">
|
||||
{activeMsg.title && (
|
||||
<h1 className="aff-classique-infos__single-title">
|
||||
{activeMsg.title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
<p className="aff-classique-infos__single-body">
|
||||
{activeMsg.content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* HORLOGE EN BAS À DROITE */}
|
||||
<div className="aff-classique-infos__footer-clock-box">
|
||||
<DigitalClock />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,337 +1,415 @@
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #006330; /* Green theme */
|
||||
background-color: #116938;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #006330;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__empty-text {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LEFT PANEL
|
||||
CONTAINER (1920x1080)
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__left {
|
||||
.aff-classique-quai-arr__container {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #116938;
|
||||
color: #ffffff;
|
||||
font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LEFT PANEL (BLANC - Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-classique-quai-arr__left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35%;
|
||||
bottom: 80px;
|
||||
background-color: #f3eef4; /* Lilac/Light Grey background */
|
||||
width: 672px;
|
||||
bottom: 85px;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 40px;
|
||||
padding: 42px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__sncf-logo {
|
||||
width: 90px;
|
||||
height: auto;
|
||||
margin-bottom: 25px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time-status {
|
||||
.aff-classique-quai-arr__left--tgv {
|
||||
bottom: 240px !important;
|
||||
padding: 32px 48px 20px 48px !important;
|
||||
}
|
||||
|
||||
/* Logo Dynamique (TER Bleu / Opérateur) */
|
||||
.aff-classique-quai-arr__brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__brand-logo {
|
||||
height: 50px;
|
||||
max-width: 220px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__sncf-voyageurs {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #116938;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Ligne Heure + Statut */
|
||||
.aff-classique-quai-arr__time-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
min-height: 100px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time {
|
||||
color: #006330; /* Green theme text */
|
||||
.aff-classique-quai-arr__time {
|
||||
color: #116938;
|
||||
font-weight: 700;
|
||||
font-size: 7.5rem;
|
||||
font-size: 105px;
|
||||
line-height: 0.92;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__status {
|
||||
color: #116938;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__status {
|
||||
color: #006330; /* Green theme status */
|
||||
.aff-classique-quai-arr__status-alt {
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
font-size: 68px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__destination {
|
||||
color: #006330; /* Green theme destination (origin) */
|
||||
.aff-classique-quai-arr__status--suppressed {
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__status--delayed {
|
||||
color: #f5a623 !important;
|
||||
}
|
||||
|
||||
/* Grande Destination / Gare Terminus */
|
||||
.aff-classique-quai-arr__destination {
|
||||
color: #116938;
|
||||
font-weight: 700;
|
||||
font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
|
||||
line-height: 1.1;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
white-space: normal; /* Allow wrapping */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 13rem; /* Prevent overflowing panel */
|
||||
font-size: 82px;
|
||||
line-height: 1.05;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 14px;
|
||||
z-index: 3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__train-info {
|
||||
.aff-classique-quai-arr__destination--multi {
|
||||
font-size: 52px !important;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
/* Services PMR, Vélo, Bar */
|
||||
.aff-classique-quai-arr__services {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
gap: 18px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 8px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__train-type {
|
||||
color: #006330; /* Green theme train type */
|
||||
/* Info Train (Type + Numéro) */
|
||||
.aff-classique-quai-arr__train-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__train-type {
|
||||
color: #116938;
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__train-number {
|
||||
color: #7e8a80; /* Grey/Green train number */
|
||||
.aff-classique-quai-arr__train-number {
|
||||
color: #7e859b;
|
||||
font-weight: 400;
|
||||
font-size: 2.2rem;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__icons {
|
||||
/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */
|
||||
.aff-classique-quai-arr__watermark {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: 20px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: right center;
|
||||
font-size: 17rem; /* Massive size */
|
||||
color: #dbebdf; /* Light green/grey watermark */
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
letter-spacing: 1.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__watermark span {
|
||||
display: inline-block;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 0 100%;
|
||||
font-size: 168px;
|
||||
font-weight: 700;
|
||||
color: #d5e5d8;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
RIGHT PANEL
|
||||
RIGHT PANEL (VERT FORÊT - Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__right {
|
||||
.aff-classique-quai-arr__right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 672px;
|
||||
right: 0;
|
||||
width: 65%;
|
||||
bottom: 80px;
|
||||
background-color: #006330; /* Green theme */
|
||||
bottom: 85px;
|
||||
background-color: #116938;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 0 30px 0;
|
||||
padding: 42px 0 20px 42px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__provenance-title {
|
||||
font-size: 3.4rem;
|
||||
color: #dbeb1a; /* Yellow provenance header */
|
||||
margin-left: 60px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 700;
|
||||
.aff-classique-quai-arr__right--tgv {
|
||||
bottom: 240px !important;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stops-list {
|
||||
.aff-classique-quai-arr__stops-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__stops-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 60px; /* Space from left edge */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* Closer spacing between stops */
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop {
|
||||
.aff-classique-quai-arr__stop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
padding-left: 50px; /* Space for rail and bullet */
|
||||
min-height: 56px;
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
/* Bullet styling */
|
||||
.aff-quais-arrivees-redesign__stop-bullet {
|
||||
/* Rail vertical vert sauge (#5c8b6b) */
|
||||
.aff-classique-quai-arr__stop-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
|
||||
margin-left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px; /* Slightly larger bullet */
|
||||
height: 16px;
|
||||
background-color: #fbec22;
|
||||
left: 20px;
|
||||
width: 28px;
|
||||
background-color: #5c8b6b;
|
||||
top: -15px;
|
||||
bottom: -15px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Le rail monte jusqu'en haut au 1er arrêt */
|
||||
.aff-classique-quai-arr__stop-item:first-child::before {
|
||||
top: -600px;
|
||||
}
|
||||
|
||||
/* Le dernier arrêt n'a pas de rail qui continue en dessous */
|
||||
.aff-classique-quai-arr__stop-item:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Pastille Jaune (#ffe500 / #fbec22) */
|
||||
.aff-classique-quai-arr__bullet {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #ffe500;
|
||||
border-radius: 50%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-text {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
/* Nom de la station */
|
||||
.aff-classique-quai-arr__stop-name {
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.5px;
|
||||
padding-left: 10px; /* Space from bullet */
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* The vertical rail segments drawn behind each bullet */
|
||||
.aff-quais-arrivees-redesign__stop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px; /* Much wider rail */
|
||||
background-color: #8fa993; /* Light green-grey rail color */
|
||||
top: -10px;
|
||||
bottom: -10px;
|
||||
z-index: 1;
|
||||
/* Gare terminus / mise en avant */
|
||||
.aff-classique-quai-arr__stop-item--dest {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Rail starts at the first bullet's center */
|
||||
.aff-quais-arrivees-redesign__stop:first-child::before {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
/* Last stop has no rail continuing below */
|
||||
.aff-quais-arrivees-redesign__stop:last-child::before {
|
||||
bottom: 50%;
|
||||
.aff-classique-quai-arr__stop-item--dest .aff-classique-quai-arr__stop-name {
|
||||
background-color: #5c8b6b;
|
||||
padding: 8px 32px 8px 65px;
|
||||
margin-left: -65px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER
|
||||
FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-quais-arrivees-redesign__footer {
|
||||
.aff-classique-quai-arr__fork-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__fork-split-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
padding-left: 65px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Rail courbe violet (#5c538f) */
|
||||
.aff-classique-quai-arr__fork-curve-rail {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -15px;
|
||||
width: 125px;
|
||||
height: 80px;
|
||||
border-left: 28px solid #5c538f;
|
||||
border-bottom: 28px solid #5c538f;
|
||||
border-bottom-left-radius: 35px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Branche droite */
|
||||
.aff-classique-quai-arr__fork-branch-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 115px;
|
||||
margin-top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Libellé "voitures 11 à 18" */
|
||||
.aff-classique-quai-arr__fork-coaches-label {
|
||||
color: #f5a623;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Boîte terminus branche droite */
|
||||
.aff-classique-quai-arr__fork-terminus-box {
|
||||
background-color: #5c538f;
|
||||
padding: 8px 30px 8px 20px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__fork-terminus-name {
|
||||
color: #ffffff;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER & HORLOGE (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-classique-quai-arr__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: #006330; /* Green theme */
|
||||
width: 1920px;
|
||||
height: 85px;
|
||||
background-color: #116938;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
z-index: 10;
|
||||
padding: 0 35px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__footer-left {
|
||||
font-size: 3rem;
|
||||
.aff-classique-quai-arr__footer-track {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-style: italic;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__footer-right {
|
||||
.aff-classique-quai-arr__footer-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__clock {
|
||||
border: 2px solid #ffffff;
|
||||
padding: 4px 14px;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
.aff-classique-quai-arr__clock {
|
||||
background-color: #00337f;
|
||||
border: 2.5px solid #ffffff;
|
||||
padding: 4px 16px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__clock-s {
|
||||
color: #f5a623;
|
||||
.aff-classique-quai-arr__clock-hm {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--long-text {
|
||||
font-size: 3.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
.aff-classique-quai-arr__clock-s {
|
||||
color: #ff8c00;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--suppressed,
|
||||
.aff-quais-arrivees-redesign__status--suppressed {
|
||||
color: #ff4c5b !important;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__time--delayed,
|
||||
.aff-quais-arrivees-redesign__status--delayed {
|
||||
color: #ff9e43 !important;
|
||||
}
|
||||
|
||||
/* Disrupted Stop styling */
|
||||
.aff-quais-arrivees-redesign__stop-text--suppressed {
|
||||
text-decoration: line-through;
|
||||
color: #ff4c5b;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-text--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge {
|
||||
font-size: 2.2rem;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge--suppressed {
|
||||
color: #ff4c5b;
|
||||
}
|
||||
|
||||
.aff-quais-arrivees-redesign__stop-status-badge--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,344 +1,415 @@
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Achemine';
|
||||
src: url('/public/fonts/achemine_bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #0b376d;
|
||||
background-color: #00337f;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #0b376d;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__empty-text {
|
||||
font-size: 6rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LEFT PANEL
|
||||
CONTAINER (1920x1080)
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__left {
|
||||
.aff-classique-quai-dep__container {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #00337f;
|
||||
color: #ffffff;
|
||||
font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
LEFT PANEL (BLANC - Conforme Image 1)
|
||||
========================================================= */
|
||||
.aff-classique-quai-dep__left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 35%;
|
||||
bottom: 80px;
|
||||
width: 672px;
|
||||
bottom: 85px;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 40px;
|
||||
padding: 42px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__sncf-logo {
|
||||
width: 90px;
|
||||
height: auto;
|
||||
margin-bottom: 25px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time-status {
|
||||
.aff-classique-quai-dep__left--tgv {
|
||||
bottom: 240px !important;
|
||||
padding: 32px 48px 20px 48px !important;
|
||||
}
|
||||
|
||||
/* Logo Dynamique */
|
||||
.aff-classique-quai-dep__brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__brand-logo {
|
||||
height: 50px;
|
||||
max-width: 220px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__sncf-voyageurs {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #00337f;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Ligne Heure + Statut */
|
||||
.aff-classique-quai-dep__time-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
min-height: 100px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time {
|
||||
color: #0b376d;
|
||||
.aff-classique-quai-dep__time {
|
||||
color: #00337f;
|
||||
font-weight: 700;
|
||||
font-size: 7.5rem;
|
||||
font-size: 105px;
|
||||
line-height: 0.92;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__status {
|
||||
color: #00337f;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__status {
|
||||
color: #0b376d;
|
||||
.aff-classique-quai-dep__status-alt {
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
font-size: 68px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__destination {
|
||||
color: #0b376d;
|
||||
.aff-classique-quai-dep__status--suppressed {
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__status--delayed {
|
||||
color: #f5a623 !important;
|
||||
}
|
||||
|
||||
/* Grande Destination */
|
||||
.aff-classique-quai-dep__destination {
|
||||
color: #00337f;
|
||||
font-weight: 700;
|
||||
font-size: 5.5rem; /* Slightly smaller to fit 2 lines if needed */
|
||||
line-height: 1.1;
|
||||
margin-bottom: 5px;
|
||||
z-index: 2;
|
||||
white-space: normal; /* Allow wrapping */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 13rem; /* Prevent overflowing panel */
|
||||
font-size: 82px;
|
||||
line-height: 1.05;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 14px;
|
||||
z-index: 3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-info {
|
||||
.aff-classique-quai-dep__destination--multi {
|
||||
font-size: 52px !important;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
/* Services PMR, Vélo, Bar */
|
||||
.aff-classique-quai-dep__services {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
gap: 18px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 8px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-type {
|
||||
color: #0b376d;
|
||||
/* Info Train (Type + Numéro) */
|
||||
.aff-classique-quai-dep__train-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__train-type {
|
||||
color: #00337f;
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__train-number {
|
||||
color: #929bb0;
|
||||
.aff-classique-quai-dep__train-number {
|
||||
color: #7e859b;
|
||||
font-weight: 400;
|
||||
font-size: 2.2rem;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__icons {
|
||||
/* Filigrane Vertical "départ" géant (orienté bas vers haut) */
|
||||
.aff-classique-quai-dep__watermark {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: 20px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
transform-origin: right center;
|
||||
font-size: 17rem; /* Massive size */
|
||||
color: #e5e8ed;
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
letter-spacing: 1.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__watermark span {
|
||||
display: inline-block;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 0 100%;
|
||||
font-size: 168px;
|
||||
font-weight: 700;
|
||||
color: #e2e8f2;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
RIGHT PANEL
|
||||
RIGHT PANEL (BLEU NUIT - Conforme Image 1)
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__right {
|
||||
.aff-classique-quai-dep__right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 672px;
|
||||
right: 0;
|
||||
width: 65%;
|
||||
bottom: 80px;
|
||||
background-color: #0b376d;
|
||||
bottom: 85px;
|
||||
background-color: #00337f;
|
||||
box-sizing: border-box;
|
||||
padding: 30px 0 30px 0;
|
||||
padding: 42px 0 20px 42px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stops-list {
|
||||
.aff-classique-quai-dep__right--tgv {
|
||||
bottom: 240px !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__stops-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__stops-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 60px; /* Space from left edge */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* Closer spacing between stops */
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop {
|
||||
.aff-classique-quai-dep__stop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
padding-left: 50px; /* Space for rail and bullet */
|
||||
min-height: 56px;
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
/* Bullet styling */
|
||||
.aff-quais-departs-redesign__stop-bullet {
|
||||
/* Rail vertical violet/indigo (#5c538f) */
|
||||
.aff-classique-quai-dep__stop-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; /* Centered on 24px rail: (24-16)/2 = 4 */
|
||||
margin-left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px; /* Slightly larger bullet */
|
||||
height: 16px;
|
||||
background-color: #fbec22;
|
||||
left: 20px;
|
||||
width: 28px;
|
||||
background-color: #5c538f;
|
||||
top: -15px;
|
||||
bottom: -15px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Le rail monte jusqu'en haut au 1er arrêt */
|
||||
.aff-classique-quai-dep__stop-item:first-child::before {
|
||||
top: -600px;
|
||||
}
|
||||
|
||||
/* Le dernier arrêt n'a pas de rail qui continue en dessous */
|
||||
.aff-classique-quai-dep__stop-item:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Pastille Jaune (#ffe500 / #fbec22) */
|
||||
.aff-classique-quai-dep__bullet {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #ffe500;
|
||||
border-radius: 50%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-text {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
/* Nom de la station */
|
||||
.aff-classique-quai-dep__stop-name {
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.5px;
|
||||
padding-left: 10px; /* Space from bullet */
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* The vertical rail segments drawn behind each bullet */
|
||||
.aff-quais-departs-redesign__stop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px; /* Much wider rail */
|
||||
background-color: #5c538f;
|
||||
top: -10px;
|
||||
bottom: -10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Rail extends all the way to the top */
|
||||
.aff-quais-departs-redesign__stop:first-child::before {
|
||||
top: -1000px;
|
||||
}
|
||||
|
||||
/* Last stop has no rail continuing below */
|
||||
.aff-quais-departs-redesign__stop:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Highlighted Destination Stop */
|
||||
.aff-quais-departs-redesign__stop--dest {
|
||||
/* Gare terminus / mise en avant */
|
||||
.aff-classique-quai-dep__stop-item--dest {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop--dest .aff-quais-departs-redesign__stop-text {
|
||||
.aff-classique-quai-dep__stop-item--dest .aff-classique-quai-dep__stop-name {
|
||||
background-color: #5c538f;
|
||||
padding: 4px 20px 4px 60px; /* Increased left padding to cover bullet */
|
||||
margin-left: -60px; /* Pull back to exactly align with left:0 */
|
||||
border-radius: 4px;
|
||||
padding: 8px 32px 8px 65px;
|
||||
margin-left: -65px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
z-index: 3; /* Behind the bullet (z-index 4) */
|
||||
z-index: 3;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER
|
||||
FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-quais-departs-redesign__footer {
|
||||
.aff-classique-quai-dep__fork-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__fork-split-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
padding-left: 65px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Rail courbe violet (#5c538f) */
|
||||
.aff-classique-quai-dep__fork-curve-rail {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -15px;
|
||||
width: 125px;
|
||||
height: 80px;
|
||||
border-left: 28px solid #5c538f;
|
||||
border-bottom: 28px solid #5c538f;
|
||||
border-bottom-left-radius: 35px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Branche droite */
|
||||
.aff-classique-quai-dep__fork-branch-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 115px;
|
||||
margin-top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Libellé "voitures 11 à 18" */
|
||||
.aff-classique-quai-dep__fork-coaches-label {
|
||||
color: #f5a623;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Boîte terminus branche droite (ex: Lille Europe) */
|
||||
.aff-classique-quai-dep__fork-terminus-box {
|
||||
background-color: #5c538f;
|
||||
padding: 8px 30px 8px 20px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__fork-terminus-name {
|
||||
color: #ffffff;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER & HORLOGE (Conforme Image 1)
|
||||
========================================================= */
|
||||
.aff-classique-quai-dep__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: #0b376d;
|
||||
width: 1920px;
|
||||
height: 85px;
|
||||
background-color: #00337f;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 40px;
|
||||
z-index: 10;
|
||||
padding: 0 35px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__footer-left {
|
||||
font-size: 3rem;
|
||||
.aff-classique-quai-dep__footer-track {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-style: italic;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__footer-right {
|
||||
.aff-classique-quai-dep__footer-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__clock {
|
||||
border: 2px solid #ffffff;
|
||||
padding: 4px 14px;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
.aff-classique-quai-dep__clock {
|
||||
background-color: #00337f;
|
||||
border: 2.5px solid #ffffff;
|
||||
padding: 4px 16px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__clock-s {
|
||||
color: #f5a623;
|
||||
.aff-classique-quai-dep__clock-hm {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--long-text {
|
||||
font-size: 3.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
.aff-classique-quai-dep__clock-s {
|
||||
color: #ff8c00;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--suppressed,
|
||||
.aff-quais-departs-redesign__status--suppressed {
|
||||
color: #ff4c5b !important;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__time--delayed,
|
||||
.aff-quais-departs-redesign__status--delayed {
|
||||
color: #ff9e43 !important;
|
||||
}
|
||||
|
||||
/* Disrupted Stop styling */
|
||||
.aff-quais-departs-redesign__stop-text--suppressed {
|
||||
text-decoration: line-through;
|
||||
color: #ff4c5b;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-text--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge {
|
||||
font-size: 2.2rem;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge--suppressed {
|
||||
color: #ff4c5b;
|
||||
}
|
||||
|
||||
.aff-quais-departs-redesign__stop-status-badge--delayed {
|
||||
color: #ff9e43;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24569,6 +24569,7 @@ function cleanStationQuery(s) {
|
||||
const STATION_ALIASES = {
|
||||
'valence tgv': '87763029',
|
||||
'valence': '87761007',
|
||||
'valence ville': '87761007',
|
||||
'paris nord': '87271007',
|
||||
'paris gare du nord': '87271007',
|
||||
'paris est': '87113001',
|
||||
@@ -24581,11 +24582,18 @@ const STATION_ALIASES = {
|
||||
'austerlitz': '87547000',
|
||||
'paris bercy': '87686667',
|
||||
'bercy': '87686667',
|
||||
'paris saint lazare': '87384008',
|
||||
'saint lazare': '87384008',
|
||||
'marseille': '87751008',
|
||||
'marseille saint charles': '87751008',
|
||||
'marseille st charles': '87751008',
|
||||
'marseille blancarde': '87751081',
|
||||
'lyon part dieu': '87723197',
|
||||
'lyon perrache': '87722025',
|
||||
'lyon jean mace': '87282624',
|
||||
'lyon gorge de loup': '87721175',
|
||||
'lyon vaise': '87721001',
|
||||
'lyon saint paul': '87721159',
|
||||
'lyon saint exupery': '87762906',
|
||||
'lyon st exupery': '87762906',
|
||||
'toulouse': '87611004',
|
||||
@@ -24614,9 +24622,73 @@ const STATION_ALIASES = {
|
||||
'besancon franche comte tgv': '87300863',
|
||||
'dijon': '87713040',
|
||||
'dijon ville': '87713040',
|
||||
'dijon centre': '87713040',
|
||||
'dijon porte neuve': '87713073',
|
||||
'dijon porte-neuve': '87713073',
|
||||
'porte neuve': '87713073',
|
||||
'cannes': '87757625',
|
||||
'cannes la bocca': '87757617',
|
||||
'nice': '87756056',
|
||||
'nice ville': '87756056',
|
||||
'nice riquier': '87756353',
|
||||
'nice saint augustin': '87756254',
|
||||
'toulon': '87755009',
|
||||
'toulon sainte musse': '87742262',
|
||||
'amiens': '87313874'
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalise un nom de gare pour comparaison stricte
|
||||
*/
|
||||
export function normalizeStation(name) {
|
||||
if (!name) return '';
|
||||
return String(name)
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/^gare\s+d['e]\s+/i, '')
|
||||
.replace(/[-_/.,()'&]+/g, ' ')
|
||||
.replace(/\bst\b/g, 'saint')
|
||||
.replace(/\bste\b/g, 'sainte')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare strictement deux noms de gares pour éviter la confusion entre
|
||||
* gares de même ville (ex: "Dijon" vs "Dijon Porte Neuve", "Lyon Part Dieu" vs "Lyon Perrache")
|
||||
*/
|
||||
export function matchStationNameStrict(a, b) {
|
||||
if (!a || !b) return false;
|
||||
const normA = normalizeStation(a);
|
||||
const normB = normalizeStation(b);
|
||||
if (!normA || !normB) return false;
|
||||
if (normA === normB) return true;
|
||||
|
||||
// Liste des qualificatifs de sous-gares / gares secondaires ou périphériques distinctes
|
||||
const distinctQualifiers = [
|
||||
'porte neuve', 'part dieu', 'perrache', 'jean mace', 'gorge de loup', 'vaise',
|
||||
'saint paul', 'saint augustin', 'riquier', 'la bocca', 'bocca', 'pont du gard',
|
||||
'sud de france', 'sainte musse', 'europe', 'flandres', 'chateaucreux', 'bellevue',
|
||||
'carnot', 'terrasse', 'estressin', 'roche', 'franche comte tgv', 'montbeliard tgv'
|
||||
];
|
||||
|
||||
// Si l'une contient un qualificatif distinct et pas l'autre -> Ne correspondent PAS
|
||||
for (const q of distinctQualifiers) {
|
||||
const aHas = normA.includes(q);
|
||||
const bHas = normB.includes(q);
|
||||
if (aHas !== bHas) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Si on retire "ville" ou "centre", vérifions si elles sont identiques
|
||||
const stripGeneric = (s) => s.replace(/\b(ville|centre|gare)\b/g, '').replace(/\s+/g, ' ').trim();
|
||||
const sA = stripGeneric(normA);
|
||||
const sB = stripGeneric(normB);
|
||||
return sA === sB;
|
||||
}
|
||||
|
||||
export function findGtfsGare(query) {
|
||||
if (!query) return null;
|
||||
const rawQ = String(query).trim();
|
||||
@@ -24633,7 +24705,11 @@ export function findGtfsGare(query) {
|
||||
if (found) return found;
|
||||
}
|
||||
|
||||
// 3. Algorithme de scoring par tokens et similarité
|
||||
// 3. Recherche par correspondance exacte nette
|
||||
const exact = GTFS_GARES.find(g => cleanStationQuery(g.nom) === qClean);
|
||||
if (exact) return exact;
|
||||
|
||||
// 4. Algorithme de scoring par tokens et similarité
|
||||
const qTokens = qClean.split(' ').filter(Boolean);
|
||||
const wantsTgv = qTokens.includes('tgv');
|
||||
|
||||
@@ -24649,8 +24725,12 @@ export function findGtfsGare(query) {
|
||||
|
||||
if (gClean === qClean) {
|
||||
score = 1000;
|
||||
} else if (gClean.startsWith(qClean)) {
|
||||
score = 500;
|
||||
} else if (matchStationNameStrict(g.nom, rawQ)) {
|
||||
score = 900;
|
||||
} else if (gClean.startsWith(qClean + ' ')) {
|
||||
// Sous-gare ayant le préfixe mais plus longue (ex: "dijon porte neuve" quand la requête est "dijon")
|
||||
// Pénaliser si l'utilisateur a tapé une requête courte exacte
|
||||
score = 400 - (gTokens.length - qTokens.length) * 50;
|
||||
} else {
|
||||
let allFound = true;
|
||||
let tokenMatches = 0;
|
||||
@@ -24665,15 +24745,15 @@ export function findGtfsGare(query) {
|
||||
}
|
||||
|
||||
if (allFound) {
|
||||
score = 200 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 5;
|
||||
score = 300 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 40;
|
||||
} else if (tokenMatches > 0) {
|
||||
score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 5;
|
||||
score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 30;
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsTgv && hasTgv) score += 150;
|
||||
if (wantsTgv && !hasTgv) score -= 200;
|
||||
if (!wantsTgv && hasTgv) score -= 50;
|
||||
if (!wantsTgv && hasTgv) score -= 100;
|
||||
|
||||
if (!wantsTgv && gClean.includes('ville') && qTokens.length === 1 && gTokens[0] === qTokens[0]) {
|
||||
score += 100;
|
||||
@@ -24707,6 +24787,8 @@ export function searchGtfsGares(query, limit = 20) {
|
||||
|
||||
if (gClean === qClean) {
|
||||
scored.push({ gare: g, score: 1000 });
|
||||
} else if (matchStationNameStrict(g.nom, query)) {
|
||||
scored.push({ gare: g, score: 950 });
|
||||
} else if (gClean.startsWith(qClean)) {
|
||||
scored.push({ gare: g, score: 800 });
|
||||
} else {
|
||||
@@ -24718,9 +24800,9 @@ export function searchGtfsGares(query, limit = 20) {
|
||||
}
|
||||
}
|
||||
if (matchCount === qTokens.length) {
|
||||
scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length });
|
||||
scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length * 10 });
|
||||
} else if (matchCount > 0) {
|
||||
scored.push({ gare: g, score: matchCount * 20 - gTokens.length });
|
||||
scored.push({ gare: g, score: matchCount * 20 - gTokens.length * 10 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+553
-89
@@ -5,23 +5,81 @@ import { findGtfsGare, searchGtfsGares } from './gtfsStations';
|
||||
const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-trip-updates';
|
||||
const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts';
|
||||
const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable';
|
||||
const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
|
||||
const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
|
||||
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 30s)
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
|
||||
const REALTIME_CACHE_TTL = 120000;
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
|
||||
let gtfsRtCache = {
|
||||
timestamp: 0,
|
||||
updates: new Map(),
|
||||
alerts: []
|
||||
};
|
||||
|
||||
// Cache mémoire pour le flux SIRI-Lite (TTL = 30s)
|
||||
// Cache mémoire pour le flux SIRI-Lite Estimated Timetable (TTL = 2 min)
|
||||
let siriLiteCache = {
|
||||
timestamp: 0,
|
||||
platforms: new Map(),
|
||||
updates: new Map()
|
||||
};
|
||||
|
||||
// Cache mémoire pour le flux SIRI-Lite Situation Exchange (TTL = 2 min)
|
||||
let situationExchangeCache = {
|
||||
timestamp: 0,
|
||||
platforms: new Map(),
|
||||
updates: new Map(),
|
||||
alerts: []
|
||||
};
|
||||
|
||||
// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
|
||||
const openDataPlatformsCache = new Map();
|
||||
|
||||
const MAJOR_HUBS = [
|
||||
'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
|
||||
'toulouse', 'nantes', 'rennes', 'nice', 'montpellier', 'nimes', 'metz',
|
||||
'nancy', 'mulhouse', 'tours', 'le mans', 'rouen', 'clermont ferrand',
|
||||
'grenoble', 'chambery', 'avignon', 'valence', 'besancon viotte',
|
||||
'saint etienne', 'toulon', 'cannes', 'antibes', 'angers'
|
||||
];
|
||||
|
||||
const MAJOR_HUB_UICS = [
|
||||
'87713040', '87686006', '87271007', '87391003', '87113001', '87384008',
|
||||
'87547000', '87686667', '87722025', '87723197', '87751008', '87286005',
|
||||
'87286013', '87212027', '87581009', '87611004', '87481002', '87471003',
|
||||
'87756056', '87773002', '87775007', '87192039', '87141002', '87182063',
|
||||
'87571000', '87444000', '87411017', '87734004', '87747006', '87741009',
|
||||
'87765008', '87318964', '87763029', '87718007', '87726000', '87755009',
|
||||
'87757625', '87757674', '87484006'
|
||||
];
|
||||
|
||||
export function isMajorHub(stationName, uic = '') {
|
||||
if (uic && MAJOR_HUB_UICS.includes(String(uic).replace(/\D/g, ''))) return true;
|
||||
if (!stationName) return false;
|
||||
const clean = String(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return MAJOR_HUBS.some(hub => clean === hub || clean.startsWith(hub + ' ') || clean.endsWith(' ' + hub));
|
||||
}
|
||||
|
||||
export function isCarNumber(num) {
|
||||
const d = String(num || '').replace(/\D/g, '');
|
||||
return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout la voie pour une circulation :
|
||||
* - Aucune voie nominale/fictive inventée : seuls les quais temps réel SNCF sont affichés.
|
||||
* - Les services par autocar affichent 'AUTOCAR' (pictogramme).
|
||||
*/
|
||||
export function resolveNominalTrack(stationName, trainNum, uic = '') {
|
||||
const rawNum = String(trainNum || '').replace(/\D/g, '');
|
||||
if (isCarNumber(rawNum)) return 'AUTOCAR';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 7000) {
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
@@ -40,12 +98,53 @@ function cleanNum(n) {
|
||||
return m ? m[0].replace(/^0+/, '') : String(n).trim();
|
||||
}
|
||||
|
||||
function extractAllTrainNumbers(journeyXml) {
|
||||
const nums = new Set();
|
||||
|
||||
// 1. TrainNumberRef
|
||||
const tnr = journeyXml.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
|
||||
if (tnr) {
|
||||
const c = cleanNum(tnr[1]);
|
||||
if (c) nums.add(c);
|
||||
}
|
||||
|
||||
// 2. VehicleJourneyName
|
||||
const vjn = journeyXml.match(/<VehicleJourneyName>([^<]+)<\/VehicleJourneyName>/);
|
||||
if (vjn) {
|
||||
const c = cleanNum(vjn[1]);
|
||||
if (c) nums.add(c);
|
||||
}
|
||||
|
||||
// 3. PublishedLineName if numeric
|
||||
const pln = journeyXml.match(/<PublishedLineName>([^<]+)<\/PublishedLineName>/);
|
||||
if (pln) {
|
||||
const m = pln[1].match(/^\d+$/);
|
||||
if (m) nums.add(cleanNum(m[0]));
|
||||
}
|
||||
|
||||
// 4. DatedVehicleJourneyRef / VehicleJourneyRef / FramedVehicleJourneyRef
|
||||
const refs = journeyXml.match(/<(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>([\s\S]*?)<\/(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>/g) || [];
|
||||
for (const r of refs) {
|
||||
const vjMatch = r.match(/FR:VehicleJourney::(\d{3,7})/i) || r.match(/VehicleJourney::(\d{3,7})/i) || r.match(/::(\d{3,7})/);
|
||||
if (vjMatch) {
|
||||
nums.add(cleanNum(vjMatch[1]));
|
||||
}
|
||||
const lrMatch = r.match(/LineRef::(\d{3,7})/i);
|
||||
if (lrMatch) {
|
||||
nums.add(cleanNum(lrMatch[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(nums);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les flux temps réel SIRI Lite SNCF (Quais réels et horaires estimés)
|
||||
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
|
||||
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
|
||||
*/
|
||||
async function fetchSiriLiteData() {
|
||||
const now = Date.now();
|
||||
if (now - siriLiteCache.timestamp < 30000 && siriLiteCache.platforms.size > 0) {
|
||||
if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
@@ -53,79 +152,112 @@ async function fetchSiriLiteData() {
|
||||
const updatesMap = new Map();
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {}, 8000);
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 25000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
|
||||
for (let i = 1; i < journeys.length; i++) {
|
||||
const j = journeys[i];
|
||||
const trainNumMatch = j.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
|
||||
const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
|
||||
const trainNum = cleanNum(rawTrainNum);
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/);
|
||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||
const trainNums = extractAllTrainNumbers(j);
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
|
||||
const isCanceled = j.includes('<Cancellation>true</Cancellation>');
|
||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||
|
||||
// Extraire les arrêts et quais (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || [];
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
const stopRefMatch = call.match(/<StopPointRef>[^:]*::(\d+)<\/StopPointRef>/) || call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null;
|
||||
const uic = uicMatch ? uicMatch[0] : '';
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
const depPlatMatch = call.match(/<DeparturePlatformName>([^<]+)<\/DeparturePlatformName>/);
|
||||
const arrPlatMatch = call.match(/<ArrivalPlatformName>([^<]+)<\/ArrivalPlatformName>/);
|
||||
const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim();
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/QuayRef>/);
|
||||
|
||||
let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
|
||||
const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Extraction des heures et calcul du retard
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
if (expDepMatch && aimDepMatch) {
|
||||
const expD = new Date(expDepMatch[1]).getTime();
|
||||
const aimD = new Date(aimDepMatch[1]).getTime();
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) {
|
||||
if (trainNum) {
|
||||
platformsMap.set(`${trainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${trainNum}`, platform);
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
}
|
||||
if (rawTrainNum) {
|
||||
platformsMap.set(`${rawTrainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${rawTrainNum}`, platform);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
}
|
||||
if (stopNameClean) {
|
||||
if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform);
|
||||
if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform);
|
||||
if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trainNum || rawTrainNum || journeyRef) {
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isCanceled
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
if (trainNum) updatesMap.set(trainNum, info);
|
||||
if (rawTrainNum) updatesMap.set(rawTrainNum, info);
|
||||
if (journeyRef) updatesMap.set(journeyRef, info);
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,12 +269,298 @@ async function fetchSiriLiteData() {
|
||||
updates: updatesMap
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite refresh error/timeout:', err);
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
|
||||
}
|
||||
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode le flux SIRI Lite Situation Exchange SNCF (Circulations à venir, perturbations, changements de voie, suppressions)
|
||||
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange
|
||||
*/
|
||||
async function fetchSituationExchangeData() {
|
||||
const now = Date.now();
|
||||
if (now - situationExchangeCache.timestamp < REALTIME_CACHE_TTL && situationExchangeCache.platforms.size > 0) {
|
||||
return situationExchangeCache;
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
const updatesMap = new Map();
|
||||
const alertsList = [];
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_SITUATION_EXCHANGE, {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 12000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const situations = xml.split('<PtSituationElement>');
|
||||
|
||||
for (let i = 1; i < situations.length; i++) {
|
||||
const s = situations[i];
|
||||
|
||||
// 1. Extraction exhaustive du train et de la mission
|
||||
const trainNums = extractAllTrainNumbers(s);
|
||||
const trainNumMatch = s.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/) || s.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || s.match(/<PublishedLineName>([^<]+)<\/PublishedLineName>/);
|
||||
const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
|
||||
const primaryTrainNum = cleanNum(rawTrainNum);
|
||||
if (primaryTrainNum && !trainNums.includes(primaryTrainNum)) {
|
||||
trainNums.push(primaryTrainNum);
|
||||
}
|
||||
|
||||
// 2. Extraction de l'arrêt impacté
|
||||
const stopRefMatch = s.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = s.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
// 3. Extraction des messages en français
|
||||
const summaryFrMatch = s.match(/<Summary[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Summary>/i) || s.match(/<Summary[^>]*>([^<]+)<\/Summary>/i);
|
||||
const summary = summaryFrMatch ? summaryFrMatch[1].trim() : '';
|
||||
|
||||
const descFrMatch = s.match(/<Description[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Description>/i) || s.match(/<Description[^>]*>([^<]+)<\/Description>/i);
|
||||
const desc = descFrMatch ? descFrMatch[1].trim() : '';
|
||||
|
||||
const messageText = desc || summary || '';
|
||||
|
||||
// 4. Détection de changement de quai / voie
|
||||
const platTagMatch = s.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName|ArrivalPlatformName|PlannedArrivalPlatformName|QuayRef)>([^<]+)<\//i);
|
||||
let platform = platTagMatch ? platTagMatch[1].replace(/^(?:voie|quai|track|platform)\s*/i, '').trim() : '';
|
||||
|
||||
if (!platform && messageText) {
|
||||
const voieTextMatch = messageText.match(/\b(?:voie|quai)\s+([A-Za-z0-9]+)\b/i);
|
||||
if (voieTextMatch) {
|
||||
platform = voieTextMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Détection de suppression
|
||||
const isCanceled = /supprim|annul|gelöscht|cancelled|interrompu/i.test(messageText) || s.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: 0,
|
||||
isCanceled: isCanceled,
|
||||
cause: summary || desc || ''
|
||||
};
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
|
||||
if (messageText) {
|
||||
alertsList.push({
|
||||
header: summary,
|
||||
text: desc || summary
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
situationExchangeCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err);
|
||||
}
|
||||
|
||||
return situationExchangeCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft)
|
||||
*/
|
||||
async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') {
|
||||
if (!uic) return new Map();
|
||||
const cacheKey = `${uic}_${mode}`;
|
||||
const now = Date.now();
|
||||
if (openDataPlatformsCache.has(cacheKey)) {
|
||||
const cached = openDataPlatformsCache.get(cacheKey);
|
||||
if (now - cached.timestamp < 30000) {
|
||||
return cached.platforms;
|
||||
}
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
|
||||
try {
|
||||
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
|
||||
const uicClean = String(uic).replace(/\D/g, '');
|
||||
const isArrival = mode === 'arrivees';
|
||||
const navitiaPath = isArrival ? 'arrivals' : 'departures';
|
||||
const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel';
|
||||
|
||||
const navitiaEndpoints = [
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100`
|
||||
];
|
||||
|
||||
const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([
|
||||
fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000)
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000);
|
||||
})
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000);
|
||||
}),
|
||||
fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000),
|
||||
fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000)
|
||||
]);
|
||||
|
||||
// 1. Décodage Navitia (API OpenData SNCF temps réel)
|
||||
if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) {
|
||||
const data = await navitiaRes.value.json();
|
||||
const list = (isArrival ? data.arrivals : data.departures) || [];
|
||||
for (const item of list) {
|
||||
const info = item.display_informations || {};
|
||||
const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || '');
|
||||
const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, '');
|
||||
|
||||
const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {};
|
||||
let platform = (
|
||||
stopPoint.platform_code ||
|
||||
info.platform ||
|
||||
''
|
||||
).toString().trim();
|
||||
|
||||
if (!platform && stopPoint.label) {
|
||||
const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i);
|
||||
if (m) platform = m[1];
|
||||
}
|
||||
|
||||
platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
const rawDt = (item.stop_date_time && (item.stop_date_time.departure_date_time || item.stop_date_time.arrival_date_time || item.stop_date_time.base_departure_date_time || item.stop_date_time.base_arrival_date_time)) || '';
|
||||
let timeHM = '';
|
||||
if (rawDt.includes('T')) {
|
||||
const tPart = rawDt.split('T')[1];
|
||||
if (tPart && tPart.length >= 4) {
|
||||
timeHM = `${tPart.slice(0, 2)}:${tPart.slice(2, 4)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Décodage OpenDataSoft SNCF v2.1
|
||||
if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) {
|
||||
const data = await odsV2Res.value.json();
|
||||
const records = data.results || data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || r;
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Décodage OpenDataSoft SNCF v1.0
|
||||
if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) {
|
||||
const data = await odsV1Res.value.json();
|
||||
const records = data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || {};
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err);
|
||||
}
|
||||
|
||||
openDataPlatformsCache.set(cacheKey, {
|
||||
timestamp: now,
|
||||
platforms: platformsMap
|
||||
});
|
||||
|
||||
return platformsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
|
||||
*/
|
||||
@@ -311,8 +729,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
||||
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
||||
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS
|
||||
const [jsonRes, rtData, siriData] = await Promise.all([
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT)
|
||||
const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
|
||||
fetch(stationUrl).catch(err => {
|
||||
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
||||
return null;
|
||||
@@ -322,8 +740,16 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
return { updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchSiriLiteData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite error:', err);
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err);
|
||||
return { platforms: new Map(), updates: new Map() };
|
||||
}),
|
||||
fetchSituationExchangeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err);
|
||||
return { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchOpenDataSncfPlatforms(uic, mode).catch(err => {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err);
|
||||
return new Map();
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -333,6 +759,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
}
|
||||
|
||||
const data = await jsonRes.json();
|
||||
const gareRef = findGtfsGare(uic);
|
||||
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
|
||||
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
|
||||
if (!Array.isArray(rawList) || rawList.length === 0) return null;
|
||||
|
||||
@@ -362,21 +790,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
if (Array.isArray(customDaysRaw)) {
|
||||
customDays = customDaysRaw;
|
||||
} else if (typeof customDaysRaw === 'string') {
|
||||
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||
}
|
||||
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD);
|
||||
if (match) return Boolean(match.circule);
|
||||
try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; }
|
||||
}
|
||||
if (customDays.includes(targetDateHyphen)) return true;
|
||||
}
|
||||
|
||||
// 3. Colonnes circule_lundi, circule_mardi...
|
||||
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||
const colName = `circule_${dayNames[targetDay]}`;
|
||||
if (typeof t[colName] !== 'undefined' && t[colName] !== null) {
|
||||
const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
|
||||
// 3. Base Supabase : circulation standard par jour de semaine
|
||||
const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi'];
|
||||
const colName = daysMap[targetDay];
|
||||
if (typeof t[colName] !== 'undefined') {
|
||||
const dd = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
|
||||
const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null;
|
||||
if (db && targetDateHyphen < db) return false;
|
||||
if (dd && targetDateHyphen < dd) return false;
|
||||
if (df && targetDateHyphen > df) return false;
|
||||
return Boolean(t[colName]);
|
||||
}
|
||||
@@ -399,21 +824,37 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper de résolution de quai
|
||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
|
||||
// Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData)
|
||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => {
|
||||
const cNum = cleanNum(trainNum);
|
||||
const rawNum = String(trainNum || '').replace(/\D/g, '');
|
||||
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
const cDest = destName ? formatStationName(destName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
const uicClean = String(stationUic || '').replace(/\D/g, '');
|
||||
const uic7 = (uicClean.length === 8 && uicClean.startsWith('87')) ? uicClean.slice(2) : '';
|
||||
|
||||
return (
|
||||
(cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) ||
|
||||
(trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) ||
|
||||
(stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) ||
|
||||
(stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) ||
|
||||
// 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1)
|
||||
(cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(uicClean && cNum && siriData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && uic7 && siriData.platforms.get(`${cNum}_${uic7}`)) ||
|
||||
(uic7 && cNum && siriData.platforms.get(`${uic7}_${cNum}`)) ||
|
||||
(cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) ||
|
||||
(tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) ||
|
||||
(stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) ||
|
||||
(rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
(tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) ||
|
||||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) ||
|
||||
// 2. SIRI-Lite Situation Exchange (Circulations à venir / Changements de quai)
|
||||
(cNum && uicClean && sxData.platforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && sxData.platforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
// 3. OpenData SNCF direct
|
||||
(cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
|
||||
(cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
@@ -423,17 +864,30 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const timeHM = parseTimeHM(rawTime) || 0;
|
||||
|
||||
// Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui)
|
||||
// Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
|
||||
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
|
||||
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
|
||||
const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
|
||||
|
||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
|
||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || (sx && sx.isCanceled) || false;
|
||||
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
|
||||
|
||||
const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
|
||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
|
||||
if (!realVoie && t.quai && typeof t.quai === 'string') {
|
||||
realVoie = t.quai.trim();
|
||||
const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig);
|
||||
const destinationStation = mode === 'arrivees' ? t.orig : t.dest;
|
||||
const isAutocar = (
|
||||
(t.type && (t.type.toUpperCase().includes('AUTOCAR') || t.type.toUpperCase().includes('CAR') || t.type.toUpperCase() === 'BUS')) ||
|
||||
(t.type_train && (t.type_train.toUpperCase().includes('AUTOCAR') || t.type_train.toUpperCase().includes('CAR') || t.type_train.toUpperCase() === 'BUS')) ||
|
||||
(t.op && (t.op.toUpperCase().includes('AUTOCAR') || t.op.toUpperCase().includes('CAR') || t.op.toUpperCase() === 'BUS')) ||
|
||||
Boolean(t.is_substitution) ||
|
||||
isCarNumber(t.num || t.numero_train || t.numero)
|
||||
);
|
||||
|
||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName, rawTime, destinationStation);
|
||||
if (!realVoie && (t.quai || t.quai_reel || t.platform || t.voie)) {
|
||||
realVoie = String(t.quai || t.quai_reel || t.platform || t.voie).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
}
|
||||
if (!realVoie) {
|
||||
realVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(currentStationName, t.num, uic);
|
||||
}
|
||||
|
||||
let statusLabel = "à l'heure";
|
||||
@@ -446,14 +900,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
etat = 'retarde';
|
||||
}
|
||||
|
||||
let cause = '';
|
||||
if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) {
|
||||
let cause = (sx && sx.cause) || '';
|
||||
if (!isTomorrow && delayMinutes > 0 && !cause) {
|
||||
if (rtData.alerts.length > 0) {
|
||||
cause = rtData.alerts[idx % rtData.alerts.length].text;
|
||||
} else if (sxData.alerts.length > 0) {
|
||||
cause = sxData.alerts[idx % sxData.alerts.length].text;
|
||||
}
|
||||
}
|
||||
|
||||
const formattedStops = (t.stops || []).map((st) => {
|
||||
const stopUic = st.uic || '';
|
||||
const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
|
||||
let stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom, st.heure, destinationStation);
|
||||
if (!stopVoie && (st.quai || st.quai_reel || st.voie || st.platform)) {
|
||||
stopVoie = String(st.quai || st.quai_reel || st.voie || st.platform).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
}
|
||||
if (!stopVoie) {
|
||||
stopVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(st.nom, t.num);
|
||||
}
|
||||
|
||||
return {
|
||||
uic: stopUic,
|
||||
@@ -550,6 +1014,14 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
resultList = remainingToday;
|
||||
}
|
||||
|
||||
if (resultList.length === 0) {
|
||||
resultList = (enrichedToday.length > 0 ? enrichedToday : rawList.map((t, idx) => mapTrain(t, idx, false))).sort((a, b) => {
|
||||
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
|
||||
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
|
||||
return aHM - bHM;
|
||||
});
|
||||
}
|
||||
|
||||
return resultList;
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Erreur de chargement GTFS station:', err);
|
||||
@@ -572,13 +1044,10 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
|
||||
let finalTrains = null;
|
||||
if (uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(uic, mode);
|
||||
}
|
||||
|
||||
// 2. Si non trouvé ou gare sans fichier, chercher avec le nom
|
||||
if (!finalTrains || finalTrains.length === 0) {
|
||||
const rawGare = gare ? gare.nom : gareName;
|
||||
const match = findGtfsGare(rawGare);
|
||||
if (match && match.uic && match.uic !== uic) {
|
||||
} else {
|
||||
// Si UIC non trouvé directement, chercher par nom
|
||||
const match = findGtfsGare(gareName);
|
||||
if (match && match.uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
|
||||
}
|
||||
}
|
||||
@@ -587,19 +1056,14 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
|
||||
finalTrains = [];
|
||||
}
|
||||
|
||||
// 3. Filtrage par quai si demandé
|
||||
// 3. Filtrage strict par quai si demandé
|
||||
if (quai) {
|
||||
const normQuai = String(quai).trim().toLowerCase();
|
||||
const matching = finalTrains.filter(t => String(t.quai || t.voie).trim().toLowerCase() === normQuai);
|
||||
if (matching.length > 0) {
|
||||
finalTrains = matching;
|
||||
} else if (finalTrains.length > 0) {
|
||||
finalTrains[0].quai = quai;
|
||||
finalTrains[0].voie = quai;
|
||||
finalTrains[0].quai_reel = quai;
|
||||
finalTrains[0]._displayPlatform = quai;
|
||||
finalTrains = [finalTrains[0]];
|
||||
}
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
const targetP = cleanP(quai);
|
||||
finalTrains = finalTrains.filter(t => {
|
||||
const q = cleanP(t.quai || t.voie || t.quai_reel || t._displayPlatform);
|
||||
return Boolean(q && q === targetP);
|
||||
});
|
||||
}
|
||||
|
||||
return { data: finalTrains, error: null };
|
||||
|
||||
Reference in new issue
Block a user