From aaec7e8d0f01e77840a8c7a98f02b061d005e89c Mon Sep 17 00:00:00 2001 From: mathis Date: Wed, 23 Sep 2026 23:26:29 +0200 Subject: [PATCH] V0.5.0. --- next.config.ts | 5 + src/app/[region]/fiches-horaires/page.tsx | 25 + src/app/[region]/gares/page.tsx | 25 + .../se-deplacer/fiches-horaires/page.tsx | 25 + src/app/[region]/se-deplacer/gares/page.tsx | 25 + .../info-trafic/info-greves/page.tsx | 25 + .../info-trafic/info-travaux/page.tsx | 25 + .../[region]/se-deplacer/info-trafic/page.tsx | 25 + src/app/fiches-horaires/page.tsx | 22 + src/app/gares/page.tsx | 22 + src/app/se-deplacer/fiches-horaires/page.tsx | 22 + src/app/se-deplacer/gares/page.tsx | 22 + .../info-trafic/info-greves/page.tsx | 22 + .../info-trafic/info-travaux/page.tsx | 22 + src/app/se-deplacer/info-trafic/page.tsx | 22 + src/components/FichesHorairesPageClient.tsx | 622 ++++ src/components/GaresPageClient.tsx | 908 ++++++ src/components/HeaderMenu.tsx | 250 +- src/components/HomeContent.tsx | 84 +- src/components/HorairesPageClient.tsx | 113 +- src/components/InfoGrevesClient.tsx | 323 ++ src/components/InfoTraficHubClient.tsx | 203 ++ src/components/InfoTravauxClient.tsx | 541 ++++ src/components/OffersPageClient.tsx | 168 +- src/components/ProchainsDepartsClient.tsx | 2662 ++++++++--------- src/components/RegionContent.tsx | 228 +- src/components/SiteFooter.tsx | 260 ++ src/components/StationDeparturesBoard.tsx | 344 +++ src/components/TimetableSearchForm.tsx | 190 +- src/lib/regionTheme.ts | 95 +- 30 files changed, 5347 insertions(+), 1978 deletions(-) create mode 100644 src/app/[region]/fiches-horaires/page.tsx create mode 100644 src/app/[region]/gares/page.tsx create mode 100644 src/app/[region]/se-deplacer/fiches-horaires/page.tsx create mode 100644 src/app/[region]/se-deplacer/gares/page.tsx create mode 100644 src/app/[region]/se-deplacer/info-trafic/info-greves/page.tsx create mode 100644 src/app/[region]/se-deplacer/info-trafic/info-travaux/page.tsx create mode 100644 src/app/[region]/se-deplacer/info-trafic/page.tsx create mode 100644 src/app/fiches-horaires/page.tsx create mode 100644 src/app/gares/page.tsx create mode 100644 src/app/se-deplacer/fiches-horaires/page.tsx create mode 100644 src/app/se-deplacer/gares/page.tsx create mode 100644 src/app/se-deplacer/info-trafic/info-greves/page.tsx create mode 100644 src/app/se-deplacer/info-trafic/info-travaux/page.tsx create mode 100644 src/app/se-deplacer/info-trafic/page.tsx create mode 100644 src/components/FichesHorairesPageClient.tsx create mode 100644 src/components/GaresPageClient.tsx create mode 100644 src/components/InfoGrevesClient.tsx create mode 100644 src/components/InfoTraficHubClient.tsx create mode 100644 src/components/InfoTravauxClient.tsx create mode 100644 src/components/SiteFooter.tsx create mode 100644 src/components/StationDeparturesBoard.tsx diff --git a/next.config.ts b/next.config.ts index e9ffa30..2615f0f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,3 +5,8 @@ const nextConfig: NextConfig = { }; export default nextConfig; + +// next.config.js +module.exports = { + allowedDevOrigins: ['192.168.1.104'], +} \ No newline at end of file diff --git a/src/app/[region]/fiches-horaires/page.tsx b/src/app/[region]/fiches-horaires/page.tsx new file mode 100644 index 0000000..990e802 --- /dev/null +++ b/src/app/[region]/fiches-horaires/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import FichesHorairesPageClient from '@/components/FichesHorairesPageClient'; + +interface RegionFichesHorairesAliasPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionFichesHorairesAliasPage({ params }: RegionFichesHorairesAliasPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement des fiches horaires...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/gares/page.tsx b/src/app/[region]/gares/page.tsx new file mode 100644 index 0000000..22a44b0 --- /dev/null +++ b/src/app/[region]/gares/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import GaresPageClient from '@/components/GaresPageClient'; + +interface RegionGaresAliasPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionGaresAliasPage({ params }: RegionGaresAliasPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement de la gare...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/se-deplacer/fiches-horaires/page.tsx b/src/app/[region]/se-deplacer/fiches-horaires/page.tsx new file mode 100644 index 0000000..1e88ea5 --- /dev/null +++ b/src/app/[region]/se-deplacer/fiches-horaires/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import FichesHorairesPageClient from '@/components/FichesHorairesPageClient'; + +interface RegionFichesHorairesPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionFichesHorairesPage({ params }: RegionFichesHorairesPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement des fiches horaires...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/se-deplacer/gares/page.tsx b/src/app/[region]/se-deplacer/gares/page.tsx new file mode 100644 index 0000000..916386b --- /dev/null +++ b/src/app/[region]/se-deplacer/gares/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import GaresPageClient from '@/components/GaresPageClient'; + +interface RegionGaresPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionGaresPage({ params }: RegionGaresPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement de la gare...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/se-deplacer/info-trafic/info-greves/page.tsx b/src/app/[region]/se-deplacer/info-trafic/info-greves/page.tsx new file mode 100644 index 0000000..0818d3e --- /dev/null +++ b/src/app/[region]/se-deplacer/info-trafic/info-greves/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import InfoGrevesClient from '@/components/InfoGrevesClient'; + +interface RegionInfoGrevesPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionInfoGrevesPage({ params }: RegionInfoGrevesPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement des informations grève...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/se-deplacer/info-trafic/info-travaux/page.tsx b/src/app/[region]/se-deplacer/info-trafic/info-travaux/page.tsx new file mode 100644 index 0000000..4ab8b5d --- /dev/null +++ b/src/app/[region]/se-deplacer/info-trafic/info-travaux/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import InfoTravauxClient from '@/components/InfoTravauxClient'; + +interface RegionInfoTravauxPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionInfoTravauxPage({ params }: RegionInfoTravauxPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement des informations travaux...

+
+ + }> + +
+ ); +} diff --git a/src/app/[region]/se-deplacer/info-trafic/page.tsx b/src/app/[region]/se-deplacer/info-trafic/page.tsx new file mode 100644 index 0000000..69d03a4 --- /dev/null +++ b/src/app/[region]/se-deplacer/info-trafic/page.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; +import InfoTraficHubClient from '@/components/InfoTraficHubClient'; + +interface RegionInfoTraficPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionInfoTraficPage({ params }: RegionInfoTraficPageProps) { + const { region } = await params; + + return ( + +
+
+

Chargement de l'info trafic...

+
+ + }> + +
+ ); +} diff --git a/src/app/fiches-horaires/page.tsx b/src/app/fiches-horaires/page.tsx new file mode 100644 index 0000000..886b243 --- /dev/null +++ b/src/app/fiches-horaires/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import FichesHorairesPageClient from '@/components/FichesHorairesPageClient'; + +export const metadata = { + title: 'Fiches horaires et travaux | TER Ferrovia', + description: 'Consultez les fiches horaires et les affiches travaux de vos lignes', +}; + +export default function FichesHorairesAliasPage() { + return ( + +
+
+

Chargement des fiches horaires...

+
+ + }> + +
+ ); +} diff --git a/src/app/gares/page.tsx b/src/app/gares/page.tsx new file mode 100644 index 0000000..43cc3ea --- /dev/null +++ b/src/app/gares/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import GaresPageClient from '@/components/GaresPageClient'; + +export const metadata = { + title: 'Gares et points d\'arrêts | TER Ferrovia', + description: 'Informations pratiques, services et horaires des gares de votre région', +}; + +export default function GaresAliasPage() { + return ( + +
+
+

Chargement de la gare...

+
+ + }> + +
+ ); +} diff --git a/src/app/se-deplacer/fiches-horaires/page.tsx b/src/app/se-deplacer/fiches-horaires/page.tsx new file mode 100644 index 0000000..98ca956 --- /dev/null +++ b/src/app/se-deplacer/fiches-horaires/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import FichesHorairesPageClient from '@/components/FichesHorairesPageClient'; + +export const metadata = { + title: 'Fiches horaires et travaux | TER Ferrovia', + description: 'Consultez les fiches horaires et les affiches travaux de vos lignes', +}; + +export default function FichesHorairesPage() { + return ( + +
+
+

Chargement des fiches horaires...

+
+ + }> + +
+ ); +} diff --git a/src/app/se-deplacer/gares/page.tsx b/src/app/se-deplacer/gares/page.tsx new file mode 100644 index 0000000..d78bba3 --- /dev/null +++ b/src/app/se-deplacer/gares/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import GaresPageClient from '@/components/GaresPageClient'; + +export const metadata = { + title: 'Gares et points d\'arrêts | TER Ferrovia', + description: 'Informations pratiques, services et horaires des gares de votre région', +}; + +export default function GaresPage() { + return ( + +
+
+

Chargement de la gare...

+
+ + }> + +
+ ); +} diff --git a/src/app/se-deplacer/info-trafic/info-greves/page.tsx b/src/app/se-deplacer/info-trafic/info-greves/page.tsx new file mode 100644 index 0000000..1568c69 --- /dev/null +++ b/src/app/se-deplacer/info-trafic/info-greves/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import InfoGrevesClient from '@/components/InfoGrevesClient'; + +export const metadata = { + title: 'Information grève | TER Ferrovia', + description: 'Consultez les prévisions de circulation et informations pratiques en cas de grève', +}; + +export default function InfoGrevesPage() { + return ( + +
+
+

Chargement des informations grève...

+
+ + }> + +
+ ); +} diff --git a/src/app/se-deplacer/info-trafic/info-travaux/page.tsx b/src/app/se-deplacer/info-trafic/info-travaux/page.tsx new file mode 100644 index 0000000..eaa6854 --- /dev/null +++ b/src/app/se-deplacer/info-trafic/info-travaux/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import InfoTravauxClient from '@/components/InfoTravauxClient'; + +export const metadata = { + title: 'Information travaux | TER Ferrovia', + description: 'Consultez les dates et impacts des chantiers travaux prévus sur votre réseau régional', +}; + +export default function InfoTravauxPage() { + return ( + +
+
+

Chargement des informations travaux...

+
+ + }> + +
+ ); +} diff --git a/src/app/se-deplacer/info-trafic/page.tsx b/src/app/se-deplacer/info-trafic/page.tsx new file mode 100644 index 0000000..fdfacb4 --- /dev/null +++ b/src/app/se-deplacer/info-trafic/page.tsx @@ -0,0 +1,22 @@ +import React, { Suspense } from 'react'; +import InfoTraficHubClient from '@/components/InfoTraficHubClient'; + +export const metadata = { + title: 'Info trafic, travaux, grève | TER Ferrovia', + description: 'Consultez l\'ensemble des informations trafic, travaux et grèves de votre réseau régional', +}; + +export default function InfoTraficPage() { + return ( + +
+
+

Chargement de l'info trafic...

+
+ + }> + +
+ ); +} diff --git a/src/components/FichesHorairesPageClient.tsx b/src/components/FichesHorairesPageClient.tsx new file mode 100644 index 0000000..8370aa5 --- /dev/null +++ b/src/components/FichesHorairesPageClient.tsx @@ -0,0 +1,622 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from 'react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; +import { RichText } from '@/components/RegionContent'; + +interface LigneItem { + id: string; + nom: string; + code?: string; + gare_depart_id?: string; + gare_arrivee_id?: string; + fh_url?: string | null; + region?: string; + actif?: boolean; +} + +interface GareItem { + id: string; + nom: string; +} + +interface TravauxItem { + id: string | number; + titre: string; + description: string; + date_debut?: string; + date_fin?: string; + jours_travaux?: string[]; + heure_debut?: string; + heure_fin?: string; + ligne_id?: string; + ligne_nom?: string; + actif?: boolean; +} + +export default function FichesHorairesPageClient({ + regionSlug = "bourgogne-franche-comte", + initialLignes = [], + initialGares = [], + initialTravaux = [], +}: { + regionSlug?: string; + initialLignes?: LigneItem[]; + initialGares?: GareItem[]; + initialTravaux?: TravauxItem[]; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + const theme = getRegionTheme(regionSlug); + + const ligneQuery = searchParams.get('ligne') || ''; + const gareQuery = searchParams.get('gare') || ''; + + // State + const [lignesList, setLignesList] = useState(initialLignes); + const [garesList, setGaresList] = useState(initialGares); + const [travauxList, setTravauxList] = useState(initialTravaux); + const [menuStructure, setMenuStructure] = useState(null); + + // Form State + const [selectedLigneId, setSelectedLigneId] = useState(''); + const [gareInput, setGareInput] = useState(''); + const [showGareSuggestions, setShowGareSuggestions] = useState(false); + const [hasSearched, setHasSearched] = useState(false); + + // Selected Travaux modal for viewing detail + const [activeTravauxModal, setActiveTravauxModal] = useState(null); + const [activeFhModal, setActiveFhModal] = useState<{ title: string; lineName: string } | null>(null); + + // Sync state from query params + useEffect(() => { + if (ligneQuery) { + setSelectedLigneId(ligneQuery); + setGareInput(''); + setHasSearched(true); + } else if (gareQuery) { + setGareInput(gareQuery); + setSelectedLigneId(''); + setHasSearched(true); + } else { + setHasSearched(false); + } + }, [ligneQuery, gareQuery]); + + // Fetch Supabase data + useEffect(() => { + const fetchData = async () => { + try { + // 1. Fetch lignes + const { data: lData } = await supabase + .from('lignes') + .select('*') + .eq('actif', true) + .is('deleted_at', null) + .order('nom', { ascending: true }); + if (lData && lData.length > 0) { + setLignesList(lData as LigneItem[]); + } + + // 2. Fetch gares + const { data: gData } = await supabase + .from('gares') + .select('id, nom') + .is('deleted_at', null) + .order('nom', { ascending: true }); + if (gData && gData.length > 0) { + setGaresList(gData as GareItem[]); + } + + // 3. Fetch travaux + const { data: tData } = await supabase + .from('travaux') + .select('*') + .eq('actif', true) + .is('deleted_at', null) + .order('date_debut', { ascending: false }); + if (tData && tData.length > 0) { + setTravauxList(tData as TravauxItem[]); + } + + // 4. Fetch menu + const { data: mData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .limit(1); + if (mData && mData.length > 0) { + setMenuStructure(mData[0].structure); + } + } catch (err) { + console.error('Error fetching fiches horaires data:', err); + } + }; + + fetchData(); + }, [regionSlug]); + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + // Autocomplete Suggestions Filter for Gare + const filteredGareSuggestions = useMemo(() => { + if (!gareInput || gareInput.trim().length === 0) return []; + const query = gareInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return garesList.filter(g => + g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").includes(query) + ); + }, [gareInput, garesList]); + + // Handle Search Submission + const handleSearch = () => { + if (selectedLigneId) { + setHasSearched(true); + router.push(`${window.location.pathname}?ligne=${encodeURIComponent(selectedLigneId)}`); + } else if (gareInput.trim()) { + setHasSearched(true); + setShowGareSuggestions(false); + router.push(`${window.location.pathname}?gare=${encodeURIComponent(gareInput.trim())}`); + } + }; + + // Find Active Selected Line + const activeLine = useMemo(() => { + if (!selectedLigneId) return null; + return lignesList.find(l => l.id === selectedLigneId || l.nom.toLowerCase() === selectedLigneId.toLowerCase()) || null; + }, [selectedLigneId, lignesList]); + + // Matching Travaux + const matchingTravaux = useMemo(() => { + if (!hasSearched) return []; + if (activeLine) { + const lineNomClean = activeLine.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return travauxList.filter(t => { + if (t.ligne_id && t.ligne_id === activeLine.id) return true; + if (t.ligne_nom) { + const tNomClean = t.ligne_nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return tNomClean.includes(lineNomClean) || lineNomClean.includes(tNomClean); + } + return false; + }); + } + + if (gareInput.trim()) { + const gareClean = gareInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return travauxList.filter(t => { + const desc = (t.description || '').toLowerCase(); + const titre = (t.titre || '').toLowerCase(); + const line = (t.ligne_nom || '').toLowerCase(); + return desc.includes(gareClean) || titre.includes(gareClean) || line.includes(gareClean); + }); + } + + return travauxList; + }, [hasSearched, activeLine, gareInput, travauxList]); + + // Format line name for official Timetable sheet + const getFicheHoraireTitle = (ligne?: LigneItem | null) => { + if (ligne) { + const nom = ligne.nom.toUpperCase().replace(/\s+/g, '-'); + return `${nom} DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026`; + } + if (gareInput.trim()) { + return `LIGNE DESSERVANT ${gareInput.toUpperCase().trim()} DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026`; + } + return "DIJON-SEURRE-BOURG EN BRESSE DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026"; + }; + + return ( +
+
+ {/* SITE HEADER & MENU */} + + +
+ {/* Breadcrumb (Images 4 & 5) */} +
+ + home + + › + Fiches horaires +
+ + {/* Centered Top Form Card Container */} +
+ {/* Train + Clock Illustration with dynamic theme colors (Images 4 & 5) */} +
+
+ {/* Train body */} + + + + + + + + + +
+ {/* Overlaid Clock Icon on Top Left */} +
+ schedule +
+
+ + {/* Title & Subtitle */} +

+ Rechercher une fiche horaire +

+

+ Consulter l'ensemble des fiches horaires, travaux et tous les horaires modifiés de votre ligne. +

+ + {/* Dual Selector: Ligne | ou | Gare (Images 4 & 5) */} +
+ {/* Ligne Selector */} +
+ +
+ + alt_route + + + + expand_more + +
+
+ + {/* Separator: | ou | */} +
+ ou +
+ + {/* Gare Selector */} +
+ +
+ + place + + { + setGareInput(e.target.value); + setShowGareSuggestions(true); + if (e.target.value) setSelectedLigneId(''); + }} + onFocus={() => setShowGareSuggestions(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSearch(); + }} + placeholder="Gare" + className="w-full bg-transparent py-3 pr-3 text-[13.5px] text-gray-800 placeholder-gray-500 outline-none" + /> + {gareInput && ( + + )} +
+ + {/* Suggestions Dropdown for Gare */} + {showGareSuggestions && filteredGareSuggestions.length > 0 && ( +
+ {filteredGareSuggestions.map((g) => ( +
{ + setGareInput(g.nom); + setShowGareSuggestions(false); + setSelectedLigneId(''); + }} + className="px-4 py-2.5 hover:bg-gray-50 cursor-pointer text-[13px] text-gray-800 transition-colors" + style={{ color: theme.primary }} + > + {g.nom} +
+ ))} +
+ )} +
+
+ + {/* Rechercher Button with Dynamic Theme Color */} + +
+ + {/* ─── STEP 2: RESULTS SECTION (Image 5) ─── */} + {hasSearched && ( +
+ {/* Section Header: En cours with theme underline */} +
+

+ En cours +

+
+ + {/* List of Results (Travaux notices + Official Timetable Sheets) */} +
+ {/* 1. Affiches Travaux Notices (Orange circular icons) */} + {matchingTravaux.length > 0 ? ( + matchingTravaux.map((trav) => ( +
setActiveTravauxModal(trav)} + className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1" + > + {/* Orange Round Icon */} + + construction + + + {/* Text */} + + Affiches Travaux {trav.ligne_nom || activeLine?.nom || 'Ligne'} {trav.date_debut && trav.date_fin ? `du ${new Date(trav.date_debut).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' })} au ${new Date(trav.date_fin).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}` : ''} + + › +
+ )) + ) : ( +
setActiveTravauxModal({ + id: 'default-travaux', + titre: `Travaux d'entretiens des voies - ${activeLine?.nom || gareInput || 'Ligne'}`, + description: `

Actuellement et jusqu'au 4 octobre du lundi au vendredi
Suppression de certains trains en milieu de journée.
Des autocars de remplacement sont mis en place entre les principales gares.

`, + ligne_nom: activeLine?.nom || gareInput || 'Ligne' + })} + className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1" + > + + construction + + + Affiches Travaux {activeLine?.nom || gareInput || 'Ligne Régionale'} du 1 septembre au 4 octobre 2026 + + › +
+ )} + + {/* 2. Official Timetable PDF / Viewer Sheet with Dynamic Theme Color */} +
setActiveFhModal({ + title: getFicheHoraireTitle(activeLine), + lineName: activeLine?.nom || gareInput || 'Ligne Régionale' + })} + className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1" + > + {/* Round Icon with dynamic theme color */} + + train + + + {/* Text */} + + {getFicheHoraireTitle(activeLine)} + + › +
+
+
+ )} +
+
+ + {/* MODAL TRAVAUX DETAILS */} + {activeTravauxModal && ( +
setActiveTravauxModal(null)} + > +
e.stopPropagation()} + > + {/* Header */} +
+
+ construction + Affiche Travaux +
+ +
+ + {/* Body */} +
+

+ {activeTravauxModal.titre} +

+ {activeTravauxModal.ligne_nom && ( +
+ Ligne : {activeTravauxModal.ligne_nom} +
+ )} +
+ +
+
+ + {/* Footer */} +
+ + +
+
+
+ )} + + {/* MODAL FICHE HORAIRE PDF / VIEWER */} + {activeFhModal && ( +
setActiveFhModal(null)} + > +
e.stopPropagation()} + > + {/* Header */} +
+
+ description + Fiche Horaire Officielle +
+ +
+ + {/* Body */} +
+
+ picture_as_pdf +
+

+ {activeFhModal.title} +

+

+ Téléchargez ou imprimez la grille horaire complète valable pour la période 2025 - 2026. +

+
+

Réseau : TER {theme.regionName} ({theme.operatorLabel})

+

Ligne : {activeFhModal.lineName}

+

Format : Document PDF Officiel

+
+
+ + {/* Footer */} +
+ + +
+
+
+ )} + + {/* REUSABLE SITE FOOTER (Image 1) */} + +
+ ); +} diff --git a/src/components/GaresPageClient.tsx b/src/components/GaresPageClient.tsx new file mode 100644 index 0000000..91e079d --- /dev/null +++ b/src/components/GaresPageClient.tsx @@ -0,0 +1,908 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from 'react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; +import TimetableSearchForm from '@/components/TimetableSearchForm'; +import StationDeparturesBoard from '@/components/StationDeparturesBoard'; + +export interface LigneTransportInfo { + nom?: string; + terminus_a?: string; + terminus_b?: string; +} + +export interface GareInformations { + adresse?: string; + parking?: boolean; + toilettes?: boolean; + espace_vtc?: boolean; + lignes_bus?: (string | LigneTransportInfo)[]; + photomaton?: boolean; + espace_taxis?: boolean; + lignes_metro?: (string | LigneTransportInfo)[]; + wifi_gratuit?: boolean; + depose_minute?: boolean; + horaires_gare?: string; + parking_velos?: boolean; + defibrillateur?: boolean; + espace_attente?: boolean; + lignes_tramway?: (string | LigneTransportInfo)[]; + objets_trouves?: boolean; + distributeur_ter?: boolean; + distributeur_tgv?: boolean; + horaires_guichet?: string; + location_voiture?: boolean; + service_assistance?: boolean; + velo_libre_service?: boolean; + galeries_marchandes?: boolean; + lignes_car_regional?: (string | LigneTransportInfo)[]; +} + +export function formatLineDetail(line: string | LigneTransportInfo): string { + if (!line) return ''; + if (typeof line === 'string') return line; + if (typeof line === 'object') { + const nom = line.nom || ''; + if (line.terminus_a && line.terminus_b) { + return `Ligne ${nom} (${line.terminus_a.trim()} - ${line.terminus_b.trim()})`; + } + if (line.terminus_a || line.terminus_b) { + return `Ligne ${nom} (${(line.terminus_a || line.terminus_b || '').trim()})`; + } + return nom ? `Ligne ${nom}` : ''; + } + return String(line); +} + +export interface GareItem { + id: string; + nom: string; + type?: string; + services?: any; + informations?: GareInformations; +} + +interface HoraireGareDesservie { + gare_id?: string; + gare_nom: string; + quai?: string | null; + heure_depart?: string | null; + heure_arrivee?: string | null; + ordre?: number; + is_depart?: boolean; + is_arrivee?: boolean; +} + +interface HoraireItem { + id: string | number; + numero_train?: string; + type_train?: string; + ligne_nom?: string; + heure_depart?: string; + heure_arrivee?: string; + gare_depart_nom?: string; + gare_arrivee_nom?: string; + gares_desservies?: HoraireGareDesservie[] | string; + jours_personnalises?: any[]; + actif?: boolean; +} + +interface ResolvedDeparture { + id: string | number; + scheduledTime: string; + destName: string; + trainLabel: string; + trainNumber: string; + track: string; + hasDelay: boolean; + delayText?: string; + hasInfo: boolean; + infoText?: string; +} + +export default function GaresPageClient({ + regionSlug = "bourgogne-franche-comte", + initialGares = [], + initialSelectedGare = null, +}: { + regionSlug?: string; + initialGares?: GareItem[]; + initialSelectedGare?: string | null; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + const theme = getRegionTheme(regionSlug); + + // Read ?gare= from query parameters or prop + const gareQuery = searchParams.get('gare') || initialSelectedGare || ''; + + // State + const [garesList, setGaresList] = useState(initialGares); + const [searchInput, setSearchInput] = useState(gareQuery); + const [selectedGareName, setSelectedGareName] = useState(gareQuery); + const [showSuggestions, setShowSuggestions] = useState(false); + const [showAllStationsModal, setShowAllStationsModal] = useState(false); + const [menuStructure, setMenuStructure] = useState(null); + + // Sync selected gare when query changes + useEffect(() => { + if (gareQuery) { + setSelectedGareName(gareQuery); + setSearchInput(gareQuery); + } else { + setSelectedGareName(''); + setSearchInput(''); + } + }, [gareQuery]); + + // Fetch Gares & Horaires from Supabase + useEffect(() => { + const fetchData = async () => { + try { + // 1. Fetch gares + const { data: gData } = await supabase + .from('gares') + .select('id, nom, type, services, informations') + .is('deleted_at', null) + .order('nom', { ascending: true }); + + if (gData && gData.length > 0) { + setGaresList(gData as GareItem[]); + } + + // 2. Fetch menu + const { data: mData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .limit(1); + if (mData && mData.length > 0) { + setMenuStructure(mData[0].structure); + } + } catch (err) { + console.error('Error fetching data for Gares page:', err); + } + }; + + fetchData(); + }, [regionSlug]); + + // Selected Gare Object from DB + const currentGareObj = useMemo(() => { + if (!selectedGareName) return null; + const cleanName = selectedGareName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return garesList.find(g => + g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim() === cleanName || + g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().includes(cleanName) || + cleanName.includes(g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim()) + ) || null; + }, [selectedGareName, garesList]); + + // Dynamic values extracted from informations column + const info: GareInformations = currentGareObj?.informations || {}; + + // Autocomplete Suggestions Filter + const filteredSuggestions = useMemo(() => { + if (!searchInput || searchInput.trim().length === 0) return []; + const query = searchInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return garesList.filter(g => + g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").includes(query) + ); + }, [searchInput, garesList]); + + // Handle Search Submission + const handleSearch = (targetName?: string) => { + const finalName = (targetName !== undefined ? targetName : searchInput).trim(); + if (!finalName) return; + setSelectedGareName(finalName); + setShowSuggestions(false); + const params = new URLSearchParams(window.location.search); + params.set('gare', finalName); + router.push(`${window.location.pathname}?${params.toString()}`); + }; + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + return ( +
+
+ {/* SITE HEADER & MENU */} + + + {/* ─── STEP 1: RECHERCHE DE GARE (Image 2) ─── */} + {!selectedGareName ? ( +
+ {/* Breadcrumb */} +
+ + home + + › + Rechercher une gare +
+ + {/* Centered Search Card Container */} +
+ {/* Train Illustration in Theme Badge */} +
+ + + + + + + + + + +
+ + {/* Title & Subtitle */} +

+ Rechercher une gare +

+

+ Informations pratiques et tableaux des départs et arrivées de plus de 5000 gares +

+ + {/* Station Search Input with Autocomplete */} +
+
+ + place + + { + setSearchInput(e.target.value); + setShowSuggestions(true); + }} + onFocus={() => setShowSuggestions(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSearch(); + }} + placeholder="Gare" + className="w-full bg-transparent py-3 pr-4 text-[14px] text-gray-900 placeholder-gray-500 outline-none" + /> + {searchInput && ( + + )} +
+ + {/* Suggestions Dropdown */} + {showSuggestions && filteredSuggestions.length > 0 && ( +
+ {filteredSuggestions.map((g) => ( +
{ + setSearchInput(g.nom); + handleSearch(g.nom); + }} + className="px-4 py-2.5 hover:bg-gray-50 cursor-pointer text-[13.5px] text-gray-800 flex items-center justify-between transition-colors" + > + {g.nom} + {g.type || 'Gare'} +
+ ))} +
+ )} +
+ + {/* Submit Button (Pill button) */} + + + {/* Link: Voir la liste des gares et points d'arrêts */} + +
+
+ ) : ( + /* ─── STEP 2: DÉTAIL DE LA GARE (Image 3 - DYNAMIQUE BDD) ─── */ +
+
+ {/* Breadcrumb */} +
+ + home + + › + + Rechercher une gare + + › + {selectedGareName} +
+ + {/* Theme Header Banner: GARE DE [NOM] */} +
+

+ GARE DE {selectedGareName} +

+
+ + {/* 1. Practical Info Block (Dynamique depuis informations) */} +
+ + {/* Adresse */} +
+ place +
+ Adresse +

+ {info.adresse && info.adresse.trim().length > 0 + ? info.adresse + : (selectedGareName.toLowerCase().includes('dijon') ? "21 cour de la gare\n21000 Dijon" : `Parvis de la Gare\n${selectedGareName}`)} +

+
+
+ + {/* Horaires d'ouverture */} +
+ schedule +
+ Horaires d'ouverture +
+ Gare +

+ {info.horaires_gare && info.horaires_gare.trim().length > 0 + ? info.horaires_gare + : "Lundi à vendredi : de 4h30 à 23h15\nDimanche et jours fériés : de 5h45 à 23h15"} +

+
+
+ Guichet +
+ {info.horaires_guichet && info.horaires_guichet.trim().length > 0 ? ( +

{info.horaires_guichet}

+ ) : ( + <> +
+ + ESPACE DE VENTE TRAIN {theme.operatorLabel} : + +

Lundi à vendredi : de 7h00 à 19h00

+

Samedi : de 8h00 à 19h00

+

Dimanche et jours fériés : de 10h00 à 19h00

+
+
+ + ESPACE DE VENTE GRANDES LIGNES : + +

Lundi à samedi : de 9h00 à 18h00

+

Dimanche et jours fériés : fermé

+
+ + )} +
+
+
+
+ + {/* Distributeurs TER & TGV */} + {(info.distributeur_ter !== false) && ( +
+ confirmation_number + Distributeur de titre de transport TER +
+ )} + {info.distributeur_tgv && ( +
+ confirmation_number + Distributeur de billets Grandes Lignes +
+ )} + + {/* Achetez vos billets sur mobile */} +
+ smartphone +
+ Achetez aussi vos billets sur votre mobile + + Rechercher un trajet + › + +
+
+
+ + {/* 2. Blue Info Banner: "Bon à savoir" */} +
+ info +
+ Bon à savoir +

+ Informations et vente de billets : le site {theme.operatorLabel} ( + + service-client.{regionSlug}.fr + + ) est à votre disposition ou par téléphone au 03 80 11 29 29 du lundi au samedi de 7h à 20h. +

+
+
+ + {/* 3. Section Accessibilité */} +
+

+ Accessibilité +

+
+ {/* Card 1 */} +
+ accessible_forward +
+ + Les services d'assistance et équipements dans cette gare + +

+ {info.service_assistance !== false + ? "Retrouvez pour cette gare les informations d'accessibilité ainsi que les équipements disponibles (ascenseur, élévateur, bande d'éveil...) sur Gares & Connexions ou dans le document ci-dessous." + : "Gare équipée de dispositifs d'accès adaptés. Contactez le service d'assistance avant votre départ."} +

+ + Consulter les équipements et accessibilité › + +
+
+ + {/* Card 2 */} +
+ info +
+ + Informations générales sur les services d'assistance en gare et les équipements à bord des trains + +

+ Découvrez comment bénéficier d'une prise en charge personnalisée pour vos trajets régionaux. +

+ + Accéder à la rubrique › + +
+
+
+
+ + {/* 4. Section Services (Dynamique selon informations) */} +
+

+ Services +

+
+ + {/* Détente / Espace attente */} + {(info.espace_attente !== false) && ( +
+ weekend +
+ Détente +
    +
  • Salon d'attente
  • +
  • Espace repos
  • +
+
+
+ )} + + {/* Boutique / Galeries */} + {(info.galeries_marchandes !== false) && ( +
+ storefront +
+ Boutique +
    +
  • Relay / vente journaux
  • +
  • Vente de pain, légumes, fromages, miel... 2 jours/semaine sur le parvis de la gare (En savoir plus >)
  • +
+
+
+ )} + + {/* Restauration */} +
+ local_cafe +
+ Restauration +
    +
  • Distributeurs de boissons / friandises
  • +
  • Restauration rapide
  • +
  • Vente à emporter
  • +
+
+
+ + {/* Objets trouvés */} + {(info.objets_trouves !== false) && ( +
+ find_in_page +
+ Objets trouvés +
    +
  • Perdu et trouvé (au guichet de la gare)
  • +
+
+
+ )} + + {/* Consigne colis */} +
+ inventory_2 +
+ Consigne colis +
    +
  • Pickup Station
  • +
+
+
+ + {/* Toilettes */} + {(info.toilettes !== false) && ( +
+ wc +
+ Toilettes +
    +
  • Toilettes payantes et accessibles pour les personnes à mobilité réduite
  • +
+
+
+ )} + + {/* Autres services (Photomaton, Defibrillateur, Wifi...) */} +
+ widgets +
+ Autres services +
    + {(info.photomaton !== false) &&
  • Cabine photographique
  • } + {(info.wifi_gratuit !== false) &&
  • Wifi gratuit en gare
  • } + {(info.defibrillateur !== false) &&
  • Défibrillateur cardiaque disponible
  • } +
  • Photocopieur
  • +
  • Boîte aux lettres à l'extérieur, sur le parvis de la gare
  • +
+
+
+ + {/* Distributeur de billets */} +
+ credit_card +
+ Distributeur de billets de banque +

Disponible dans le hall principal.

+
+
+ +
+
+ + {/* 5. Section Intermodalité & Transports en commun (Dynamique) */} +
+

+ Transport en commun +

+
+ + {/* Bus Card */} +
+
+ directions_bus + Bus +
+
+

Réseau Urbain :

+ {Array.isArray(info.lignes_bus) && info.lignes_bus.length > 0 ? ( +
+ {info.lignes_bus.map((b, idx) => ( +

• {formatLineDetail(b)}

+ ))} +
+ ) : ( +

Lignes urbaines desservant la gare

+ )} +

Tél : 03 80 11 29 29

+
+
+ + {/* Tramway Card (if exists or default) */} + {(Array.isArray(info.lignes_tramway) && info.lignes_tramway.length > 0) || selectedGareName.toLowerCase().includes('dijon') ? ( +
+
+ tram + Tramway +
+
+

Réseau Tram :

+ {Array.isArray(info.lignes_tramway) && info.lignes_tramway.length > 0 ? ( +
+ {info.lignes_tramway.map((t, idx) => ( +

• {formatLineDetail(t)}

+ ))} +
+ ) : ( +
+

• Ligne T1 (Gare SNCF - Dijon Valmy / Quetigny)

+

• Ligne T2 (Chenôve / Valmy Zenon - Grand Sud)

+
+ )} +

Tél : 03 80 11 29 29

+
+
+ ) : null} + + {/* Car Card */} +
+
+ airport_shuttle + Car +
+
+

Réseau {theme.operatorLabel}

+ {Array.isArray(info.lignes_car_regional) && info.lignes_car_regional.length > 0 ? ( +
+ {info.lignes_car_regional.map((c, idx) => ( +

• {formatLineDetail(c)}

+ ))} +
+ ) : ( +

Autocars régionaux réguliers

+ )} +

Renseignements sur le portail régional

+
+
+
+ + {/* Vélo Card */} + {(info.parking_velos !== false || info.velo_libre_service !== false) && ( + <> +

Vélo

+
+
+
+ pedal_bike + Vélo +
+
+

• Abris vélos sécurisés

+

• Location vélos en libre-service

+
+
+
+ + )} + + {/* Voiture & Taxi Header & Cards */} +

Voiture & Taxi

+
+ + {/* Voiture Card */} +
+
+ directions_car + Voiture +
+
+ {(info.depose_minute !== false) &&

• Dépose-minute devant la gare

} + {(info.parking !== false) &&

• Parking courte et longue durée

} +

• Bornes de recharge électrique

+ {(info.location_voiture !== false) &&

• Loueurs : AVIS, Enterprise, Europcar, Hertz, Sixt

} +
+
+ + {/* Taxi Card */} +
+
+ local_taxi + Taxi +
+
+

• Station Taxi devant la gare

+ {(info.espace_vtc !== false) &&

• Espace de prise en charge VTC

} +

Tél Taxi : 03 80 41 41 12

+
+
+ +
+
+
+ + {/* 6. GREY WIDGETS SECTION: TIMETABLE FORM + LIVE DEPARTURES (SIDE-BY-SIDE) */} +
+
+
+ + {/* Left: Timetable Search Form Widget */} +
+

+ search + Rechercher un horaire depuis {selectedGareName} +

+ +
+ + {/* Right: Prochains départs en gare de [Gare] Reusable Component */} +
+ +
+ +
+
+
+ + {/* 7. SECTION HORAIRES ET TRAVAUX */} +
+

+ HORAIRES ET TRAVAUX +

+ + Retrouvez les fiches horaires et travaux + › + +
+ + {/* 8. SECTION INSPIRATION DE VOYAGES */} +
+

+ Inspiration de voyages en TER {theme.regionName} +

+
+ {/* Column 1: Destinations favorites */} +
+

Destinations favorites

+
    +
  • Dijon > Besançon
  • +
  • Dijon > Laroche-Migennes
  • +
  • Dijon > Chalon-sur-Saône
  • +
  • Dijon > Paris
  • +
  • Dijon > Dole
  • +
+
+ + {/* Column 2: Gares à proximité */} +
+

Gares à proximité de {selectedGareName}

+
    +
  • Dijon-Porte-Neuve
  • +
  • Genlis
  • +
  • Ouges
  • +
  • Gevrey-Chambertin
  • +
  • Vougeot
  • +
+
+ + {/* Column 3: Gares les plus recherchées */} +
+

Gares les plus recherchées

+
    +
  • Paris Bercy
  • +
  • Besançon Viotte
  • +
  • Laroche-Migennes
  • +
  • Sens
  • +
  • Belfort
  • +
+
+
+
+ +
+ )} +
+ + {/* MODAL: Voir toutes les gares */} + {showAllStationsModal && ( +
setShowAllStationsModal(false)} + > +
e.stopPropagation()} + > +
+

Liste des gares du réseau

+ +
+
+ {garesList.map((g) => ( + + ))} +
+
+
+ )} + + {/* REUSABLE SITE FOOTER (Image 1) */} + +
+ ); +} diff --git a/src/components/HeaderMenu.tsx b/src/components/HeaderMenu.tsx index c7f7e6c..1ca49a5 100644 --- a/src/components/HeaderMenu.tsx +++ b/src/components/HeaderMenu.tsx @@ -44,7 +44,22 @@ const DEFAULT_MENU: MenuItem[] = [ { id: 'def-1-3', label: 'Gares', - url: '/#gares' + url: '/se-deplacer/gares' + }, + { + id: 'def-1-4', + label: 'Fiches horaires', + url: '/se-deplacer/fiches-horaires' + }, + { + id: 'def-1-5', + label: 'Info trafic', + url: '/se-deplacer/info-trafic', + sous_elements: [ + { id: 'def-1-5-1', label: 'Info trafic général', url: '/se-deplacer/info-trafic' }, + { id: 'def-1-5-2', label: 'Information travaux', url: '/se-deplacer/info-trafic/info-travaux' }, + { id: 'def-1-5-3', label: 'Information grève', url: '/se-deplacer/info-trafic/info-greves' } + ] } ], sous_sous_elements: [] @@ -57,16 +72,16 @@ const DEFAULT_MENU: MenuItem[] = [ { id: 'def-2-1', label: 'Fiches horaires', - url: '/#fiches-horaires' + url: '/se-deplacer/fiches-horaires' }, { id: 'def-2-2', label: 'Info trafic', - url: '/#info-trafic', + url: '/se-deplacer/info-trafic', sous_elements: [ - { id: 'def-2-2-1', label: 'Travaux toutes lignes', url: '/#info-trafic' }, - { id: 'def-2-2-2', label: 'Travaux ligne Paris - Laroche - Dijon - Lyon', url: '/#info-trafic' }, - { id: 'def-2-2-3', label: 'Adaptation axe Lyon - Paray-le-Monial - Nevers', url: '/#info-trafic' } + { id: 'def-2-2-1', label: 'Info trafic général', url: '/se-deplacer/info-trafic' }, + { id: 'def-2-2-2', label: 'Information travaux', url: '/se-deplacer/info-trafic/info-travaux' }, + { id: 'def-2-2-3', label: 'Information grève', url: '/se-deplacer/info-trafic/info-greves' } ] } ] @@ -204,11 +219,20 @@ export default function HeaderMenu({ return list; }; + const chunkSubItemsIntoColumns = (subItems: MenuItem[], maxPerCol = 3): MenuItem[][] => { + if (!subItems || subItems.length === 0) return []; + const columns: MenuItem[][] = []; + for (let i = 0; i < subItems.length; i += maxPerCol) { + columns.push(subItems.slice(i, i + maxPerCol)); + } + return columns; + }; + const activeSubItems = activeItem ? getSubItems(activeItem) : []; return (
- {/* DESKTOP NAVIGATION BAR (IMAGE 1 & 2) */} + {/* DESKTOP NAVIGATION BAR */} - - {/* MOBILE BAR & ACCORDION DRAWER */} -
- Menu Régional - -
- - {isMobileMenuOpen && ( -
- {items.map((item, idx) => { - const subItems = getSubItems(item); - const isExpanded = !!expandedMobileIndices[idx]; - - return ( -
-
- setMobileMenuOpen(false)} - className="text-[14px] font-semibold text-gray-800 hover:text-[#006837] py-1" - > - {item.label} - - - {subItems.length > 0 && ( - - )} -
- - {isExpanded && subItems.length > 0 && ( -
- {subItems.map((sub, sIdx) => { +
+ {chunkSubItemsIntoColumns(activeSubItems, 3).map((column, colIdx) => ( +
+ {column.map((sub, sIdx) => { const nestedList = getSubItems(sub); return ( -
+
setMobileMenuOpen(false)} - className="text-[13px] font-medium text-gray-700 hover:text-[#006837] block py-0.5" + className="text-[13.5px] font-semibold text-gray-900 transition-colors block hover:underline" + style={{ color: theme.primary }} > {sub.label} + {nestedList.length > 0 && ( -
+
{nestedList.map((nested, nIdx) => ( setMobileMenuOpen(false)} - className="text-[12px] text-gray-500 hover:text-gray-800 block py-0.5" + className="text-[12px] text-gray-600 hover:text-black block transition-colors leading-relaxed font-normal" > {nested.label} @@ -383,10 +329,102 @@ export default function HeaderMenu({ ); })}
- )} + ))}
- ); - })} +
+
+ )} + + + {/* ─── MOBILE FULLSCREEN MENU (STRICTLY CONFORME IMG 2) ─── */} + {isMobileMenuOpen && ( +
+
+ {/* Top Bar: ✕ Menu (Img 2) */} +
+ + Menu +
+ + {/* Navigation List Items with chevrons (Img 2) */} +
+ {items.map((item, idx) => { + const subItems = getSubItems(item); + const isExpanded = !!expandedMobileIndices[idx]; + + return ( +
+
{ + if (subItems.length > 0) { + toggleMobileIndex(idx); + } else { + setMobileMenuOpen(false); + } + }} + > + { + if (subItems.length > 0) { + e.preventDefault(); + toggleMobileIndex(idx); + } else { + setMobileMenuOpen(false); + } + }} + className="flex-1 text-gray-800 font-normal" + > + {item.label} + + + {subItems.length > 0 && isExpanded ? 'expand_less' : 'chevron_right'} + +
+ + {/* Sub-items if expanded */} + {isExpanded && subItems.length > 0 && ( +
+ {subItems.map((sub, sIdx) => ( + setMobileMenuOpen(false)} + className="block py-2.5 text-[13px] text-[#006837] font-medium hover:underline" + > + {sub.label} + + ))} +
+ )} +
+ ); + })} +
+
+ + {/* Bottom Link: Mes commandes (Img 2) */} +
+ setMobileMenuOpen(false)} + className="flex items-center justify-between text-[#006837] text-[13.5px] font-normal py-3 px-4 hover:bg-gray-50 rounded-lg cursor-pointer" + > +
+ receipt_long + Mes commandes +
+ chevron_right + +
)}
diff --git a/src/components/HomeContent.tsx b/src/components/HomeContent.tsx index e796fed..42894a2 100644 --- a/src/components/HomeContent.tsx +++ b/src/components/HomeContent.tsx @@ -4,6 +4,7 @@ import React from 'react'; import Link from 'next/link'; import { WcsButton, WcsCard, WcsCardBody } from 'wcs-react'; import ClientWcsProvider from '@/components/ClientWcsProvider'; +import SiteFooter from '@/components/SiteFooter'; interface RegionData { nom_site: string; @@ -19,48 +20,51 @@ interface GeneralParams { export default function HomeContent({ general }: { general: GeneralParams | null }) { return ( -
-
-
-

- FERROVIA Regional App -

-

- Découvrez les horaires, les trajets et les informations trafic de votre région. - Cette application unifie les sites régionaux TER avec une interface moderne. -

-
- - {!general ? ( - - -

- Impossible de charger la configuration depuis Supabase. -

-
-
- ) : ( -
- {Object.entries(general.regions).map(([slug, regionInfo]) => ( - - -

- {regionInfo.nom_site || `FERROVIA ${slug}`} -

-

- Accédez au portail régional pour {slug.replace(/-/g, ' ')}. -

- - - Accéder - - -
-
- ))} +
+
+
+
+

+ FERROVIA Regional App +

+

+ Découvrez les horaires, les trajets et les informations trafic de votre région. + Cette application unifie les sites régionaux TER avec une interface moderne. +

- )} + + {!general ? ( + + +

+ Impossible de charger la configuration depuis Supabase. +

+
+
+ ) : ( +
+ {Object.entries(general.regions).map(([slug, regionInfo]) => ( + + +

+ {regionInfo.nom_site || `FERROVIA ${slug}`} +

+

+ Accédez au portail régional pour {slug.replace(/-/g, ' ')}. +

+ + + Accéder + + +
+
+ ))} +
+ )} +
+
); diff --git a/src/components/HorairesPageClient.tsx b/src/components/HorairesPageClient.tsx index 37f49b3..cff5a01 100644 --- a/src/components/HorairesPageClient.tsx +++ b/src/components/HorairesPageClient.tsx @@ -3,9 +3,12 @@ import React, { useState } from 'react'; import Link from 'next/link'; import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; import TimetableSearchForm from '@/components/TimetableSearchForm'; import ClientWcsProvider from '@/components/ClientWcsProvider'; +import { getRegionTheme } from '@/lib/regionTheme'; + interface HorairesPageClientProps { regionSlug: string; regionData: { @@ -16,84 +19,19 @@ interface HorairesPageClientProps { menuStructure?: any; } -const REGION_CONFIGS: Record = { - 'bourgogne-franche-comte': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CBFC/2020-04/1920x680-BFC-Champs-Balles-de-Paille.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/bfc/headerLogo.svg', - name: 'Bourgogne-Franche-Comté' - }, - 'occitanie': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/COC/2021-04/1920x680_Occitanie_0.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/occitanie/headerLogo.svg', - name: 'Occitanie' - }, - 'provence-alpes-cote-azur': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CZOU/2021-05/1920x680_ZOU.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/paca/headerLogo.svg', - name: 'Provence-Alpes-Côte d\'Azur' - }, - 'sud-provence-alpes-cote-d-azur': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CZOU/2021-05/1920x680_ZOU.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/paca/headerLogo.svg', - name: 'Provence-Alpes-Côte d\'Azur' - }, - 'bretagne': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CBR/2020-04/1920x680_Bretagne.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/bretagne/headerLogo.svg', - name: 'Bretagne' - }, - 'grand-est': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CGE/2020-04/1920x680_Grand_Est.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/ge/headerLogo.svg', - name: 'Grand-Est' - }, - 'normandie': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CNO/2020-04/1920x680_Normandie.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/normandie/headerLogo.svg', - name: 'Normandie' - }, - 'hauts-de-france': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CHDF/2020-04/1920x680_Hauts_de_France.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/hdf/headerLogo.svg', - name: 'Hauts-de-France' - }, - 'pays-de-la-loire': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CPDL/2020-04/1920x680_Pays_de_la_Loire.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/aleop/headerLogo.svg', - name: 'Pays de la Loire' - }, - 'nouvelle-aquitaine': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CNA/2020-04/1920x680_Nouvelle_Aquitaine.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/na/headerLogo.svg', - name: 'Nouvelle-Aquitaine' - }, - 'centre-val-de-loire': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CCEN/2020-04/1920x680_Centre.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/cen/headerLogo.svg', - name: 'Centre-Val de Loire' - }, - 'auvergne-rhone-alpes': { - heroImage: 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CAURA/2020-04/1920x680_AURA.jpg', - logoUrl: 'https://www.ter.sncf.com/assets/medias/aura/headerLogo.svg', - name: 'Auvergne-Rhône-Alpes' - } -}; - -const DEFAULT_HERO = 'https://mmt.vsct.fr/sites/default/files/styles/swt_hero/public/swt/CBFC/2020-04/1920x680-BFC-Champs-Balles-de-Paille.jpg'; - export default function HorairesPageClient({ - regionSlug, + regionSlug = 'bourgogne-franche-comte', regionData, menuStructure, }: HorairesPageClientProps) { - const cfg = REGION_CONFIGS[regionSlug] || { - heroImage: DEFAULT_HERO, - logoUrl: regionData.logo_header_url || '', - name: regionSlug.replace(/-/g, ' ').toUpperCase() - }; + const theme = getRegionTheme(regionSlug); - const logoUrl = regionData.logo_header_url || cfg.logoUrl; - const heroImage = cfg.heroImage; + const regionName = regionData?.nom_site + ? regionData.nom_site.replace('FERROVIA ', '').trim() + : regionSlug.replace(/-/g, ' ').toUpperCase(); + + const logoUrl = regionData?.logo_header_url || ''; + const heroImage = theme.heroImage; const [searchFeedback, setSearchFeedback] = useState(null); @@ -121,7 +59,7 @@ export default function HorairesPageClient({
- home + home > Rechercher un horaire @@ -137,34 +75,29 @@ export default function HorairesPageClient({ >
{searchFeedback && ( -
+
{searchFeedback} - +
)} {/* Timetable Search Form card pinned to LEFT side (Image 1) */}
- +
{/* ================================================================== */} - {/* 5. FOOTER */} + {/* 5. FOOTER (IMAGE 1) */} {/* ================================================================== */} -
-
- Accueil {cfg.name} - • - Rechercher un horaire - • - Info Trafic - • - Actualités -
-

© FERROVIA - Tous droits réservés.

-
+
diff --git a/src/components/InfoGrevesClient.tsx b/src/components/InfoGrevesClient.tsx new file mode 100644 index 0000000..19b3aa2 --- /dev/null +++ b/src/components/InfoGrevesClient.tsx @@ -0,0 +1,323 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; + +interface InfoGrevesClientProps { + regionSlug?: string; +} + +export default function InfoGrevesClient({ + regionSlug = 'bourgogne-franche-comte' +}: InfoGrevesClientProps) { + const theme = getRegionTheme(regionSlug); + const [menuStructure, setMenuStructure] = useState(null); + const [affluenceModalOpen, setAffluenceModalOpen] = useState(false); + + useEffect(() => { + const fetchMenu = async () => { + try { + const cleanSlug = regionSlug || 'bourgogne-franche-comte'; + const { data: rData } = await supabase + .from('regions') + .select('id, slug') + .or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`); + + const rIds = rData ? rData.map((r: any) => r.id) : []; + let query = supabase.from('menus_regionaux').select('structure').eq('actif', true); + if (rIds.length > 0) { + query = query.in('region_id', rIds); + } + const { data: mData } = await query.limit(1); + if (mData && mData.length > 0 && mData[0].structure) { + setMenuStructure(mData[0].structure); + } + } catch (e) { + console.error('Error fetching regional menu:', e); + } + }; + fetchMenu(); + }, [regionSlug]); + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + return ( +
+
+ {/* SITE HEADER */} + + + {/* ─── MAIN CONTENT ─── */} +
+ + {/* Breadcrumbs */} + + + {/* ─── TOP HERO BANNER (Img 3) ─── */} +
+
+ + Mouvement social & Prévisions + +

+ GRÈVE : LA CIRCULATION DES TRAINS {theme.operatorLabel?.toUpperCase() || 'MOBIGO'} +

+

+ Retrouvez les prévisions de circulation et l'ensemble des informations pratiques pour vos voyages lors des mouvements sociaux. +

+
+ + {/* Background Decorative Icon */} +
+ campaign +
+
+ + {/* ─── SECTION 1: LES PERTURBATIONS SUR VOTRE LIGNE (Img 3) ─── */} +
+

+ + LES PERTURBATIONS SUR VOTRE LIGNE +

+ +
+
+
+ + info + +
+
+

+ Lors d'un préavis de grève, les plans de transport adaptés sont élaborés ligne par ligne pour vous assurer le meilleur niveau de service possible. +

+

+ 🕒 Les prévisions de circulation détaillées pour le lendemain sont consultables chaque jour à partir de 17h00. +

+

+ Nous vous recommandons vivement de vérifier la circulation effective de votre train ou car avant de vous déplacer en gare. +

+
+
+
+ +
+
+ Consultez la fiche horaire du jour ou recherchez votre numéro de train : +
+ + search + Vérifier un horaire + +
+
+ + {/* ─── SECTION 2: CONDITIONS D'ÉCHANGE & REMBOURSEMENT ─── */} +
+

+ + ÉCHANGE ET REMBOURSEMENT +

+ +
+
+

+ confirmation_number + Billets unitaires +

+

+ Si votre train est supprimé en raison d'un mouvement social, votre billet est utilisable sur n'importe quel autre train de la même journée, ou remboursable sans frais sur simple demande sur le canal d'achat initial. +

+
+ +
+

+ card_membership + Abonnés régionaux +

+

+ Pour les abonnés mensuels ou annuels, des mesures de compensation ou de dédommagement peuvent être mises en place par la Région en cas de perturbations prolongées. +

+
+
+
+ + {/* ─── SECTION 3: BESOIN D'INFOS SUPPLÉMENTAIRES ? (Img 3) ─── */} +
+

+ BESOIN D'INFOS SUPPLÉMENTAIRES ? +

+ +
+ {/* Card Phone */} +
+
+ + call + +
+
+

+ Par téléphone +

+

+ Contactez le centre de relation client au 0 800 872 872 (service & appel gratuits), du lundi au vendredi de 7h à 20h et le samedi de 9h à 17h. +

+
+
+ + {/* Card Twitter / X */} +
+
+ 𝕏 +
+
+

+ Sur X (ex-Twitter) +

+

+ Suivez le fil @{theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'}_Train sur X pour suivre l'info trafic en direct pendant les grèves. +

+
+
+
+
+ + {/* ─── SECTION 4: VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR (Img 3) ─── */} +
+

+ VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR +

+ +
+ {/* Card 1: Information Travaux */} +
+
+ INFORMATION TRAVAUX +
+
+

+ Consultez les dates et impacts des chantiers prévus sur vos lignes. +

+
+ + Plus d'infos + +
+
+
+ + {/* Card 2: Affluence Voyageurs */} +
+
+ AFFLUENCE VOYAGEURS +
+
+

+ Consultez les prévisions d'affluence pour voyager en toute sérénité. +

+
+ +
+
+
+
+
+ +
+
+ + {/* Affluence Modal */} + {affluenceModalOpen && ( +
+
+
+ + groups + +
+

+ Affluence Voyageurs +

+

+ Consultez l'affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires. +

+
+ +
+
+
+ )} + + {/* SITE FOOTER */} + +
+ ); +} diff --git a/src/components/InfoTraficHubClient.tsx b/src/components/InfoTraficHubClient.tsx new file mode 100644 index 0000000..7c38e54 --- /dev/null +++ b/src/components/InfoTraficHubClient.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; + +interface InfoTraficHubClientProps { + regionSlug?: string; +} + +export default function InfoTraficHubClient({ + regionSlug = 'bourgogne-franche-comte' +}: InfoTraficHubClientProps) { + const theme = getRegionTheme(regionSlug); + const [menuStructure, setMenuStructure] = useState(null); + const [affluenceModalOpen, setAffluenceModalOpen] = useState(false); + + useEffect(() => { + const fetchMenu = async () => { + try { + const cleanSlug = regionSlug || 'bourgogne-franche-comte'; + const { data: rData } = await supabase + .from('regions') + .select('id, slug') + .or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`); + + const rIds = rData ? rData.map((r: any) => r.id) : []; + let query = supabase.from('menus_regionaux').select('structure').eq('actif', true); + if (rIds.length > 0) { + query = query.in('region_id', rIds); + } + const { data: mData } = await query.limit(1); + if (mData && mData.length > 0 && mData[0].structure) { + setMenuStructure(mData[0].structure); + } + } catch (e) { + console.error('Error fetching regional menu:', e); + } + }; + fetchMenu(); + }, [regionSlug]); + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + const cards = [ + { + id: 'travaux', + title: 'INFORMATION TRAVAUX', + description: 'Consultez les dates et impacts des chantiers prévus sur vos lignes.', + link: getRegionPath('/se-deplacer/info-trafic/info-travaux'), + isExternal: false, + }, + { + id: 'adaptations', + title: 'ADAPTATIONS EXCEPTIONNELLES', + description: 'Retrouvez les fiches horaires spécifiques ou les adaptations temporaires sur vos lignes.', + link: getRegionPath('/se-deplacer/fiches-horaires'), + isExternal: false, + }, + { + id: 'affluence', + title: 'AFFLUENCE VOYAGEURS', + description: 'Consultez les prévisions d\'affluence à bord des trains pour voyager en toute sérénité.', + link: '#', + onClick: () => setAffluenceModalOpen(true), + isExternal: false, + }, + { + id: 'greve', + title: 'INFORMATION GRÈVE', + description: 'Consultez les prévisions de circulation lors des mouvements sociaux.', + link: getRegionPath('/se-deplacer/info-trafic/info-greves'), + isExternal: false, + }, + ]; + + return ( +
+
+ {/* SITE HEADER */} + + + {/* ─── MAIN CONTENT CONTAINER ─── */} +
+ + {/* Breadcrumbs */} + + + {/* Page Title & Subtitle */} +
+

+ L'INFO TRAFIC TRAIN {theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'} +

+

+ Retrouvez l'ensemble des informations concernant la circulation des TRAINS {theme.operatorLabel || 'Mobigo'}. +

+
+ + {/* ─── 4 CARDS GRID (Img 1 Layout) ─── */} +
+ {cards.map((card) => ( +
+ {/* Green Card Header */} +
+ {card.title} +
+ + {/* Card Body */} +
+

+ {card.description} +

+ +
+ {card.onClick ? ( + + ) : ( + + Plus d'infos + + )} +
+
+
+ ))} +
+ +
+
+ + {/* Affluence Modal */} + {affluenceModalOpen && ( +
+
+
+ + groups + +
+

+ Affluence Voyageurs +

+

+ Consultez l'affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires. +

+
+ +
+
+
+ )} + + {/* SITE FOOTER */} + +
+ ); +} diff --git a/src/components/InfoTravauxClient.tsx b/src/components/InfoTravauxClient.tsx new file mode 100644 index 0000000..25de99a --- /dev/null +++ b/src/components/InfoTravauxClient.tsx @@ -0,0 +1,541 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; + +interface LigneItem { + id: string; + nom: string; + code?: string; + couleur?: string; +} + +interface TravauxItem { + id: string; + titre: string; + description?: string; + ligne_id?: string; + date_debut?: string; + date_fin?: string; + url_affiche_pdf?: string; + actif?: boolean; + type_travaux?: string; +} + +interface InfoTravauxClientProps { + regionSlug?: string; +} + +const DEFAULT_LIGNES: LigneItem[] = [ + { id: '1', nom: 'Dijon - Besançon - Belfort', code: 'Ligne 01', couleur: '#006837' }, + { id: '2', nom: 'Paris - Dijon - Lyon', code: 'Ligne 02', couleur: '#004d7a' }, + { id: '3', nom: 'Dijon - Nevers - Tours', code: 'Ligne 03', couleur: '#7b1fa2' }, + { id: '4', nom: 'Besançon - La Chaux-de-Fonds (Ligne des Horlogers)', code: 'Ligne 04', couleur: '#c2185b' }, + { id: '5', nom: 'Dole - Saint-Claude (Ligne des Hirondelles)', code: 'Ligne 05', couleur: '#d84315' }, + { id: '6', nom: 'Belfort - Épinal', code: 'Ligne 06', couleur: '#00838f' }, +]; + +export default function InfoTravauxClient({ + regionSlug = 'bourgogne-franche-comte' +}: InfoTravauxClientProps) { + const theme = getRegionTheme(regionSlug); + const [menuStructure, setMenuStructure] = useState(null); + const [lignesList, setLignesList] = useState(DEFAULT_LIGNES); + const [travauxList, setTravauxList] = useState([]); + const [selectedTravaux, setSelectedTravaux] = useState(null); + const [modalOpen, setModalOpen] = useState(false); + const [affluenceModalOpen, setAffluenceModalOpen] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + const cleanSlug = regionSlug || 'bourgogne-franche-comte'; + const { data: rData } = await supabase + .from('regions') + .select('id, slug') + .or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`); + + const rIds = rData ? rData.map((r: any) => r.id) : []; + + // 1. Fetch menu + let queryMenu = supabase.from('menus_regionaux').select('structure').eq('actif', true); + if (rIds.length > 0) { + queryMenu = queryMenu.in('region_id', rIds); + } + const { data: mData } = await queryMenu.limit(1); + if (mData && mData.length > 0 && mData[0].structure) { + setMenuStructure(mData[0].structure); + } + + // 2. Fetch lignes + const { data: lData } = await supabase + .from('lignes') + .select('*') + .eq('actif', true) + .is('deleted_at', null) + .order('nom', { ascending: true }); + if (lData && lData.length > 0) { + setLignesList(lData as LigneItem[]); + } + + // 3. Fetch travaux + const { data: tData } = await supabase + .from('travaux') + .select('*') + .eq('actif', true) + .is('deleted_at', null) + .order('date_debut', { ascending: false }); + if (tData && tData.length > 0) { + setTravauxList(tData as TravauxItem[]); + } + } catch (err) { + console.error('Error fetching info travaux data:', err); + } + }; + + fetchData(); + }, [regionSlug]); + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + const handleOpenTravaux = (ligne: LigneItem) => { + const found = travauxList.find(t => t.ligne_id === ligne.id); + if (found) { + setSelectedTravaux(found); + } else { + // Default placeholder travaux info for this line + setSelectedTravaux({ + id: `travaux-${ligne.id}`, + titre: `Travaux de maintenance et renouvellement de voie - ${ligne.nom}`, + description: `Des travaux de modernisation et d'entretien des voies ferrées sont en cours sur l'axe ${ligne.nom}. Les horaires de certains trains peuvent être modifiés et des cars de substitution sont mis en place.`, + date_debut: 'En cours', + date_fin: '31 décembre 2026', + ligne_id: ligne.id, + }); + } + setModalOpen(true); + }; + + return ( +
+
+ {/* SITE HEADER */} + + + {/* ─── MAIN CONTENT ─── */} +
+ + {/* Breadcrumbs */} + + + {/* ─── TOP HERO BANNER (Img 2) ─── */} +
+
+ + Info Réseau & Chantiers + +

+ LES TRAVAUX : TOUT SAVOIR POUR VOS VOYAGES EN TRAIN {theme.operatorLabel?.toUpperCase() || 'MOBIGO'} +

+

+ Consultez les adaptations et retrouvez vos trains / cars {theme.operatorLabel || 'Mobigo'} non concernés par les travaux sur les lignes de {theme.regionName || 'Bourgogne-Franche-Comté'}. +

+
+ + {/* Background Decorative Icon */} +
+ construction +
+
+ + {/* ─── SECTION 1: COMPRENDRE LES TRAVAUX (Img 2) ─── */} +
+

+ + COMPRENDRE LES TRAVAUX +

+ +
+
+

+ SNCF Réseau réalise chaque année des centaines de chantiers de modernisation et de régénération des voies, des caténaires, de la signalisation et des ouvrages d'art sur le réseau ferré régional. +

+

+ Ces travaux indispensables permettent de renforcer la sécurité, d'améliorer la régularité de vos trains du quotidien et de préparer le réseau aux mobilités futures. +

+

+ Pour réaliser ces opérations en toute sécurité, la circulation ferroviaire est ponctuellement interrompue ou ralentie. Des plans de transport adaptés et des substitutions routières par autocar sont systématiquement mis en place pour assurer la continuité de vos trajets. +

+
+ +
+
+
+ {/* High quality rail maintenance photo */} + Chantier travaux ferroviaires +
+ + Modernisation des voies ferrées + +
+
+
+
+
+ + {/* ─── SECTION 2: IMPACTS SUR LES CIRCULATIONS & VÉRIFIER HORAIRE (Img 2) ─── */} +
+
+

+ IMPACTS SUR LES CIRCULATIONS +

+

+ Avant votre départ, nous vous recommandons de vérifier si votre train habituel circule ou s'il est remplacé par un autocar de substitution. +

+
+
+ + search + Vérifier un horaire + +
+
+ + {/* ─── SECTION 3: ZOOM SUR LES PRINCIPAUX CHANTIERS (Img 2) ─── */} +
+
+

+ + ZOOM SUR LES PRINCIPAUX CHANTIERS +

+

+ Consultez les affiches travaux détaillées et les fiches horaires modifiées par ligne : +

+
+ +
+ {lignesList.map((ligne, idx) => { + const lineTravaux = travauxList.filter(t => t.ligne_id === ligne.id); + const hasTravaux = lineTravaux.length > 0; + + return ( +
+
+
+
+
+ + {ligne.code ? `${ligne.code} : ` : ''}{ligne.nom} + +
+

+ {hasTravaux + ? `${lineTravaux.length} chantier(s) en cours ou programmé(s)` + : 'Travaux de maintenance réguliers'} +

+
+
+ +
+ +
+
+ ); + })} +
+
+ + {/* ─── SECTION 4: BESOIN D'INFOS SUPPLÉMENTAIRES ? (Img 2) ─── */} +
+

+ BESOIN D'INFOS SUPPLÉMENTAIRES ? +

+ +
+ {/* Card Phone */} +
+
+ + call + +
+
+

+ Par téléphone +

+

+ Contactez le centre de relation client au 0 800 872 872 (service & appel gratuits), du lundi au vendredi de 7h à 20h et le samedi de 9h à 17h. +

+
+
+ + {/* Card Twitter / X */} +
+
+ 𝕏 +
+
+

+ Sur X (ex-Twitter) +

+

+ Suivez le fil @{theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'}_Train sur X pour suivre l'info trafic en temps réel 7j/7 de 6h à 20h. +

+
+
+
+
+ + {/* ─── SECTION 5: VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR (Img 2) ─── */} +
+

+ VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR +

+ +
+ {/* Card 1: Information Grève */} +
+
+ INFORMATION GRÈVE +
+
+

+ Consultez les prévisions de circulation lors des mouvements sociaux. +

+
+ + Plus d'infos + +
+
+
+ + {/* Card 2: Adaptations exceptionnelles */} +
+
+ ADAPTATIONS EXCEPTIONNELLES +
+
+

+ Retrouvez les fiches horaires spécifiques ou les adaptations temporaires. +

+
+ + Plus d'infos + +
+
+
+ + {/* Card 3: Affluence Voyageurs */} +
+
+ AFFLUENCE VOYAGEURS +
+
+

+ Consultez les prévisions d'affluence pour voyager en toute sérénité. +

+
+ +
+
+
+
+
+ +
+
+ + {/* ─── TRAVAUX DETAIL MODAL ─── */} + {modalOpen && selectedTravaux && ( +
+
+
+
+
+ construction +
+
+

+ {selectedTravaux.titre} +

+ + Période : {selectedTravaux.date_debut || 'En cours'} {selectedTravaux.date_fin ? `au ${selectedTravaux.date_fin}` : ''} + +
+
+ +
+ +
+ {selectedTravaux.description} +
+ +
+ {selectedTravaux.url_affiche_pdf ? ( + + download + Télécharger l'affiche travaux (PDF) + + ) : ( + + schedule + Voir les fiches horaires modifiées + + )} + +
+
+
+ )} + + {/* Affluence Modal */} + {affluenceModalOpen && ( +
+
+
+ + groups + +
+

+ Affluence Voyageurs +

+

+ Consultez l'affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires. +

+
+ +
+
+
+ )} + + {/* SITE FOOTER */} + +
+ ); +} diff --git a/src/components/OffersPageClient.tsx b/src/components/OffersPageClient.tsx index 782c856..da59aab 100644 --- a/src/components/OffersPageClient.tsx +++ b/src/components/OffersPageClient.tsx @@ -4,7 +4,9 @@ import React, { useState, useEffect } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; import SiteHeader from '@/components/SiteHeader'; +import SiteFooter from '@/components/SiteFooter'; interface OffersPageClientProps { regionSlug?: string; @@ -175,6 +177,7 @@ export default function OffersPageClient({ }: OffersPageClientProps) { const router = useRouter(); const searchParams = useSearchParams(); + const theme = getRegionTheme(regionSlug); // Search parameters from URL const departure = searchParams.get('departure') || 'Dijon'; @@ -524,7 +527,10 @@ export default function OffersPageClient({ }, [departure, arrival, outwardDate, outwardTime]); const handleModifySearch = () => { - const searchUrl = regionSlug ? `/${regionSlug}/horaires` : '/horaires'; + const qp = searchParams.toString(); + const searchUrl = regionSlug + ? `/${regionSlug}/horaires${qp ? `?${qp}` : ''}` + : `/horaires${qp ? `?${qp}` : ''}`; router.push(searchUrl); }; @@ -706,7 +712,7 @@ export default function OffersPageClient({ {/* Train Icon & Number (Under Pagny on Desktop, Under Dijon on Mobile) */}
- directions_railway + directions_railway {offer.trainType} {offer.trainNumber}
@@ -719,7 +725,8 @@ export default function OffersPageClient({ @@ -803,7 +820,8 @@ export default function OffersPageClient({
@@ -843,7 +861,10 @@ export default function OffersPageClient({
- + person
@@ -868,12 +889,13 @@ export default function OffersPageClient({
- {/* Green price button at bottom right of card */} + {/* Price button at bottom right of card */}
- {/* Station Search Input Box with Autocomplete */} -
-
- search + {/* Station Search Input Box with Autocomplete (Image 1) */} +
+
+ search handleSearchInputChange(e.target.value)} - onFocus={() => stationQuery && setShowDropdown(true)} + onChange={(e) => { + setStationQuery(e.target.value); + setShowDropdown(true); + if (e.target.value.trim().length > 0) { + const q = e.target.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + const filtered = stationsList.filter(s => + s.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").includes(q) + ); + setFilteredOptions(filtered); + } else { + setFilteredOptions(stationsList); + } + }} + onFocus={() => { + setShowDropdown(true); + setFilteredOptions(stationsList); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleShowTimetable(stationQuery); + } + }} placeholder="Rechercher une gare" className="w-full bg-transparent text-[14px] text-gray-900 outline-none placeholder:text-gray-400" /> {stationQuery && ( )}
- {/* Autocomplete Dropdown */} + {/* Dropdown Station List */} {showDropdown && filteredOptions.length > 0 && ( -
+
{filteredOptions.map((st) => ( -
{ setStationQuery(st.nom); - setShowDropdown(false); handleShowTimetable(st.nom); }} - className="px-4 py-2.5 text-[14px] text-gray-800 hover:bg-[#eef5e6] hover:text-[#006837] cursor-pointer flex items-center justify-between border-b border-gray-50 last:border-0" + className="w-full text-left px-4 py-2.5 text-[14px] text-gray-800 hover:bg-[#edf4d2]/60 hover:text-[#006837] flex items-center justify-between cursor-pointer" > {st.nom} - chevron_right -
+ chevron_right + ))}
)}
- {/* Action Button: "Afficher les horaires" */} + {/* Action Button: "Afficher les horaires" (Image 1) */} + {/* Bottom "Haut de page" Pill (Image 1) */} +
+ +
+
) : ( - /* STEP 2: TIMETABLE & DETAILED HORAIRE VIEW (STRICTLY IMAGE 1 & IMAGE 2 DESIGN) */ -
+ /* ════════════════════════════════════════════════════════════════ */ + /* ÉTAPE 2 : TABLEAU D'HORAIRES POUR LA GARE CHOISIE */ + /* ════════════════════════════════════════════════════════════════ */ +
- {/* Header Title Row (Image 1) */} -
+ {/* Header Title Row */} +
-

- Prochains {activeSubTab === 'arrivees' ? 'arrivées' : 'départs'} +

+ {activeSubTab === 'arrivees' ? 'Prochaines arrivées' : 'Prochains départs'}

-

+

Gare {selectedStation}

- {/* Top Right Action Links (Image 1) */} -
+ {/* Desktop Top Right Action Links */} +
+ + {/* Mobile 3 Dots Button */} +
+ +
- {/* TRAVAUX INFO BANNER (If active travaux exist) */} - {travauxList.length > 0 && ( -
-
- construction - Info Travaux : {travauxList[0].titre} -
-

- {travauxList[0].description} -

-
- )} - - {/* Sub Header Tabs: Départs SNCF / Arrivées SNCF (Image 1) */} -
+ {/* Tabs: Départs SNCF / Arrivées SNCF */} +
- {/* ─── MAIN TABLE (STRICTLY IMAGE 1 & IMAGE 2) ─── */} -
+ {/* ─── MAIN TABLE CARD ─── */} +
- {/* Table Header (Top yellow-green border line matching Image 1) */} -
+ {/* Table Header with yellow-gold underline */} +
{activeSubTab === 'arrivees' ? 'Arrivée' : 'Départ'} {activeSubTab === 'arrivees' ? 'Provenance' : 'Destination'} - Mode - Voie + Mode + Voie
- {/* ════ DÉPARTS TABLE ════ */} - {activeSubTab === 'departs' && ( -
- {loading ? ( -
- refresh -

Chargement des prochains départs en gare de {selectedStation}...

-
- ) : departures.length > 0 ? ( - departures.map((dep, dIdx) => { - const isExpanded = expandedId === dep.id; - const showDateHeader = dep.dateLabel && (dIdx === 0 || departures[dIdx - 1].dateIso !== dep.dateIso); + {/* Top Notification Banner: "Il n'y a plus de train ce jour..." */} + {noTrainsToday && activeSubTab === 'departs' && ( +
+ + i + + Il n'y a plus de train ce jour au départ de cette gare. +
+ )} - // Resolve perturbation banner style - const pertStyle = dep.perturbationType - ? getPerturbationBannerStyle(dep.perturbationType) - : null; + {/* Timetable Rows List */} +
+ {loading ? ( +
+ Chargement des horaires... +
+ ) : currentItems.length === 0 ? ( +
+ Aucun train trouvé pour cette gare. +
+ ) : ( + currentItems.map((item: any, dIdx: number) => { + const isExpanded = expandedId === item.id; + const showDateHeader = item.dateLabel && (dIdx === 0 || currentItems[dIdx - 1]?.dateIso !== item.dateIso); + const isTGV = (item.modeLabel || '').toUpperCase().includes('TGV'); + const hasInfo = Boolean(item.perturbationType === 'information' || item.perturbationDetails || item.isSupprime || item.delayText); - return ( - + const displayTime = item.departTime || item.arrivalTime || '08:00'; + const displayTitle = item.destination || item.provenance || 'Destination'; - {/* Date Separator Row (Image 1: e.g. "Lundi 21 septembre") */} - {showDateHeader && ( -
- {dep.dateLabel} + const chosenArrivalName = selectedArrivalStops[item.id] || item.stops[item.stops.length - 1]?.gareNom || displayTitle; + + // Index of chosen destination stop in the timeline + const chosenStopIndex = item.stops?.findIndex((s: StopItem) => s.gareNom.toLowerCase().trim() === chosenArrivalName.toLowerCase().trim()); + const activeHighlightIndex = chosenStopIndex !== -1 ? chosenStopIndex : -1; + + return ( + + + {/* Date Separator Row with yellow-gold underline */} + {showDateHeader && ( +
+ {item.dateLabel} +
+ )} + +
+ {/* Compact Train Row */} +
setExpandedId(isExpanded ? null : item.id)} + className="px-3.5 py-1.5 sm:px-4 sm:py-2 grid grid-cols-[56px_1fr_45px_36px] sm:grid-cols-[70px_1fr_260px_60px_50px] items-center gap-1 sm:gap-0 hover:bg-gray-50/75 cursor-pointer text-[13.5px]" + > + {/* Column 1: Time */} +
+ {item.isSupprime ? ( + {displayTime} + ) : item.revisedTime ? ( +
+ {displayTime} + {item.revisedTime} +
+ ) : ( + displayTime + )} +
+ + {/* Column 2: Destination/Provenance + Mode (on mobile) */} +
+ + {displayTitle} + + + {/* Mobile Mode sub-line */} +
+ + {item.modeIcon === 'bus' ? 'directions_bus' : 'directions_railway'} + + {item.modeLabel} {item.trainNumber} + {item.trainBadge && ( + + {item.trainBadge} + + )} +
+
+ + {/* Column 3: Mode (Desktop only) */} +
+ + {item.modeIcon === 'bus' ? 'directions_bus' : 'directions_railway'} + + {item.modeLabel} {item.trainNumber} + {item.trainBadge && ( + + {item.trainBadge} + + )} +
+ + {/* Column 4: Voie (prominently visible on mobile & desktop) */} +
+ + {item.voie} + +
+ + {/* Column 5: Action Button */} +
+ {isTGV ? ( +
+ + {isExpanded ? 'expand_less' : 'chevron_right'} + +
+ ) : ( +
+ shopping_basket + + {isExpanded ? 'expand_less' : 'expand_more'} + +
+ )} +
+
+ + {/* Information Banner on closed row */} + {hasInfo && !isExpanded && ( +
setExpandedId(isExpanded ? null : item.id)} + className="relative bg-[#e8f4fc] px-4 py-1.5 sm:px-6 sm:py-1.5 text-[12px] text-[#0055a5] flex items-center gap-2 cursor-pointer border-t border-b border-[#d8ebf9]" + > +
+ + i + + + {item.isSupprime ? 'Train supprimé' : (item.delayText || 'Information')} +
)} -
- - {/* Main Departure Row */} -
setExpandedId(isExpanded ? null : dep.id)} - className="px-3.5 py-3 sm:px-5 sm:py-3 grid grid-cols-2 sm:grid-cols-[72px_1fr_260px_72px_52px] items-center gap-2 sm:gap-0 hover:bg-gray-50/60 cursor-pointer text-[14px]" - > - {/* Time */} -
- {dep.revisedTime ? ( -
- - {dep.departTime} - - - {dep.revisedTime} + {/* ─── EXPANDED TIMELINE ACCORDION (STRICTLY CONFORME IMG 2) ─── */} + {isExpanded && ( +
+ + {/* Header */} +

+ Liste des gares +

+
+ + desservies par le {item.modeIcon === 'bus' ? 'CAR' : 'TRAIN'} {(item.modeLabel || 'MOBIGO').toUpperCase().replace(/^(TRAIN|CAR)\s+/i, '')} {item.trainNumber || ''} + + {item.trainBadge && ( + + {item.trainBadge} + + )} +
+

+ {item.operator || 'Opéré par SNCF Voyageurs - 1187'} +

+ + {/* Information description box with full HTML support */} + {item.perturbationDetails && ( +
+
+ + i + Information :
- ) : ( - - {dep.departTime} - - )} -
- - {/* Voie & Toggle (Mobile) */} -
- - Voie {dep.voie} - -
- shopping_basket - - {isExpanded ? 'expand_less' : 'expand_more'} - -
-
- - {/* Destination */} -
- {dep.destination} -
- - {/* Mode Icon + Label + Badge */} -
- - {dep.modeIcon === 'bus' ? 'directions_bus' : 'directions_railway'} - - {dep.modeLabel} {dep.trainNumber} - {dep.trainBadge && ( - - {dep.trainBadge} - - )} -
- - {/* Voie (Desktop) */} -
- {dep.voie} -
- - {/* Green Pill Button (Image 1: basket + chevron) */} -
-
- shopping_basket - - {isExpanded ? 'expand_less' : 'expand_more'} - -
-
-
- - {/* ─── PERTURBATION INFO BANNER (Image 1: light blue bg + wavy notch) ─── */} - {pertStyle && !isExpanded && ( -
- {/* Left-side wavy notch chevrons (Image 1 style) */} -
- - - -
-
- - {pertStyle.iconText} - - - {dep.delayText || dep.perturbationDetails || pertStyle.badgeLabel} - -
-
- )} - - {/* ─── EXPANDED DETAIL VIEW (Image 2 style) ─── */} - {isExpanded && ( -
- - {/* Expanded Banner for Perturbation (Image 2 style) */} - {pertStyle && ( -
-
-
- - {pertStyle.iconText === '!' ? 'error' : 'schedule'} - - - {pertStyle.badgeLabel} - -
- {dep.perturbationDetails && ( -
- {dep.perturbationDetails} -
- )} + {/<[a-z][\s\S]*>/i.test(item.perturbationDetails) ? ( +
+ ) : ( +
+ {item.perturbationDetails}
-
- )} - -

- Liste des gares -

-

- desservies par le {dep.modeIcon === 'bus' ? 'car' : 'train'} TER {dep.trainNumber} - {dep.trainBadge && ( - - {dep.trainBadge} - )} -

-

- {dep.operator || 'Opéré par SNCF Voyageurs - 1187'} -

+
+ )} - {/* Stops Timeline (Image 1 style) */} -
-
- - Départ - Gare - Voie -
+ {/* Table of stops with 100% continuous yellow line */} +
+ + {/* Table Subheader */} +
+ + Départ + + Gare + Voie +
-
- {/* Vertical timeline line */} -
+ {/* Stops Container with Single Unbroken Continuous Vertical Line */} +
+
+ + {/* Stop Rows */} +
+ {item.stops.map((stop: StopItem, sIdx: number) => { + const isOrigin = sIdx === 0; + const isSelected = chosenArrivalName === stop.gareNom; + const isHighlighted = activeHighlightIndex !== -1 && sIdx <= activeHighlightIndex; - {dep.stops.map((stop, sIdx) => { - const isFirst = sIdx === 0; return ( -
- {/* Radio Button Column */} -
- {!isFirst && ( -
+
+ {/* Column 1: Green circular radio selector ring */} +
+ {!isOrigin && ( + )}
- {/* Time Column */} - - {stop.departTime} + {/* Column 2: Departure time */} + { + if (!isOrigin) setSelectedArrivalStops(prev => ({ ...prev, [item.id]: stop.gareNom })); + }} + className={`font-normal text-gray-900 ${!isOrigin ? 'cursor-pointer' : ''}`} + > + {stop.departTime || stop.arriveeTime} - {/* Timeline & Station Name Column */} -
-
- {/* Yellow Timeline Dot */} - - - - {stop.gareNom} - -
- - {/* Perturbation sub-block */} - {stop.perturbationType && ( -
-
- - {stop.perturbationType === 'arret_supprime' ? 'error' : stop.perturbationType === 'retard' ? 'schedule' : 'info'} - - - {stop.perturbationType === 'arret_supprime' ? 'Arrêt supprimé' - : stop.perturbationType === 'retard' ? `Retard estimé de ${stop.retardMinutes || 0} min` - : stop.isNouvelleOrigine ? 'Nouvelle gare de départ' - : stop.isNouveauTerminus ? 'Nouveau terminus' - : 'Information'} - -
- {stop.perturbationDetails && ( - {stop.perturbationDetails} - )} -
- )} + {/* Column 3: Solid Snap Dot */} +
+
- {/* Voie Column */} - + {/* Column 4: Station name */} + { + if (!isOrigin) setSelectedArrivalStops(prev => ({ ...prev, [item.id]: stop.gareNom })); + }} + className={`font-normal text-gray-900 ${!isOrigin ? 'cursor-pointer hover:underline' : ''} ${ + isSelected ? 'font-semibold text-gray-950' : '' + }`} + > + {stop.gareNom} + + + {/* Column 5: Voie */} + {stop.voie}
@@ -1477,309 +1712,60 @@ export default function ProchainsDepartsClient({ })}
+
- {/* Buy Ticket Widget (Image 1 style) */} -
-

+ {/* Purchase Section */} +
+
+

Vous souhaitez acheter un trajet ?

-

+

Sélectionnez votre gare d'arrivée dans la liste ci-dessus

- -
-
- person - 1 Voyageur, Sans carte -
- edit -
+ + {/* Passenger Selection Pill */} +
+
+ person + 1 Voyageur, Sans carte +
+ edit +
+ + {/* Buy button */} +
- )} -

- - ); - }) - ) : ( -
- Aucun départ trouvé en gare de {selectedStation}. -
- )} -
- )} - - {/* ════ ARRIVÉES TABLE ════ */} - {activeSubTab === 'arrivees' && ( -
- {loading ? ( -
- refresh -

Chargement des prochaines arrivées en gare de {selectedStation}...

-
- ) : arrivals.length > 0 ? ( - arrivals.map((arr, aIdx) => { - const isExpanded = expandedId === arr.id; - const showDateHeader = arr.dateLabel && (aIdx === 0 || arrivals[aIdx - 1].dateIso !== arr.dateIso); - - const pertStyle = arr.perturbationType - ? getPerturbationBannerStyle(arr.perturbationType) - : null; - - return ( - - - {/* Date Separator */} - {showDateHeader && ( -
- {arr.dateLabel}
)} -
- {/* Main Arrival Row */} -
setExpandedId(isExpanded ? null : arr.id)} - className="px-3.5 py-3 sm:px-5 sm:py-3 grid grid-cols-2 sm:grid-cols-[72px_1fr_260px_72px_52px] items-center gap-2 sm:gap-0 hover:bg-gray-50/60 cursor-pointer text-[14px]" - > - {/* Arrival Time */} -
- {arr.revisedTime ? ( -
- - {arr.arrivalTime} - - - {arr.revisedTime} - -
- ) : ( - - {arr.arrivalTime} - - )} -
+
+ + ); + }) + )} +
- {/* Voie & Toggle (Mobile) */} -
- - Voie {arr.voie} - -
- shopping_basket - - {isExpanded ? 'expand_less' : 'expand_more'} - -
-
- - {/* Provenance */} -
- {arr.provenance} -
- - {/* Mode Icon + Label + Badge */} -
- - {arr.modeIcon === 'bus' ? 'directions_bus' : 'directions_railway'} - - {arr.modeLabel} {arr.trainNumber} - {arr.trainBadge && ( - - {arr.trainBadge} - - )} -
- - {/* Voie (Desktop) */} -
- {arr.voie} -
- - {/* Green Pill Button */} -
-
- shopping_basket - - {isExpanded ? 'expand_less' : 'expand_more'} - -
-
-
- - {/* Perturbation info banner */} - {pertStyle && !isExpanded && ( -
-
- - - -
-
- - {pertStyle.iconText} - - - {arr.delayText || arr.perturbationDetails || pertStyle.badgeLabel} - -
-
- )} - - {/* Expanded Detail (Image 1 style) */} - {isExpanded && ( -
- - {/* Expanded Banner for Perturbation (Image 2 style) */} - {pertStyle && ( -
-
-
- - {pertStyle.iconText === '!' ? 'error' : 'schedule'} - - - {pertStyle.badgeLabel} - -
- {arr.perturbationDetails && ( -
- {arr.perturbationDetails} -
- )} -
-
- )} - -

- Liste des gares -

-

- desservies par le {arr.modeIcon === 'bus' ? 'car' : 'train'} TER {arr.trainNumber} - {arr.trainBadge && ( - - {arr.trainBadge} - - )} -

-

- {arr.operator || 'Opéré par SNCF Voyageurs - 1187'} -

- - {/* Stops Timeline (Image 1 & 3 style) */} -
-
- - Départ - Gare - Voie -
- -
- {/* Vertical timeline line */} -
- - {arr.stops.map((stop, sIdx) => { - const isLast = sIdx === arr.stops.length - 1; - return ( -
- {/* Radio Button Column */} -
- {!isLast && ( -
- )} -
- - {/* Time Column */} - - {stop.departTime} - - - {/* Timeline & Station Name Column */} -
-
- {/* Yellow Timeline Dot */} - - - - {stop.gareNom} - -
- - {/* Perturbation sub-block */} - {stop.perturbationType && ( -
-
- - {stop.perturbationType === 'arret_supprime' ? 'error' : stop.perturbationType === 'retard' ? 'schedule' : 'info'} - - - {stop.perturbationType === 'arret_supprime' ? 'Arrêt supprimé' - : stop.perturbationType === 'retard' ? `Retard estimé de ${stop.retardMinutes || 0} min` - : stop.isNouvelleOrigine ? 'Nouvelle gare de départ' - : stop.isNouveauTerminus ? 'Nouveau terminus' - : 'Information'} - -
- {stop.perturbationDetails && ( - {stop.perturbationDetails} - )} -
- )} -
- - {/* Voie Column */} - - {stop.voie} - -
- ); - })} -
-
- - {/* Buy Ticket Widget (Image 1 style) */} -
-

- Vous souhaitez acheter un trajet ? -

-

- Sélectionnez votre gare d'arrivée dans la liste ci-dessus -

- -
-
- person - 1 Voyageur, Sans carte -
- edit -
-
-
- )} - -
- - ); - }) - ) : ( -
- Aucune arrivée trouvée en gare de {selectedStation}. -
- )} -
- )} +
+ {/* Back to Top */} +
+
@@ -1788,78 +1774,8 @@ export default function ProchainsDepartsClient({
- {/* ─── SCROLL TO TOP BUTTON ─── */} -
- -
- - {/* ─── FOOTER ─── */} -
-
- - {/* Region Switcher */} -
- -
- - {/* Social Icons */} -
- facebook - X - photo_camera - play_circle - chat -
- - {/* Links Row */} -
- CGU/CGV TRAIN Mobigo - • - CGU / CGV Site Internet - • - Confidentialité/Cookies - • - Gares - • - Lignes - • - MOBIGO - • - Accessibilité : partiellement conforme - • - Mentions légales - • - Plan du site - • - Contacts -
- -
- Demande d'information et réclamations • Données personnelles -
- -
-
+ {/* Footer */} +
); } diff --git a/src/components/RegionContent.tsx b/src/components/RegionContent.tsx index f0850a5..b294c26 100644 --- a/src/components/RegionContent.tsx +++ b/src/components/RegionContent.tsx @@ -7,7 +7,9 @@ import { supabase } from '@/lib/supabase'; import { getRegionTheme } from '@/lib/regionTheme'; import SiteHeader from '@/components/SiteHeader'; import HeaderMenu from '@/components/HeaderMenu'; +import SiteFooter from '@/components/SiteFooter'; import TimetableSearchForm from '@/components/TimetableSearchForm'; +import StationDeparturesBoard from '@/components/StationDeparturesBoard'; interface RegionData { nom_site: string; @@ -962,6 +964,7 @@ export default function RegionContent({ {/* ─── REUSABLE TIMETABLE SEARCH FORM MODULE (IMAGE 1) ─── */} setSearchSuccessMessage(`Recherche d'horaire lancée de "${params.departure || 'Gare'}" à "${params.arrival || 'Gare'}" pour le ${params.outwardDate}`)} /> @@ -1161,139 +1164,17 @@ export default function RegionContent({
- {/* ─── COL 1 : PROCHAINS DÉPARTS (TABLE HORAIRES SUPABASE) ─── */} -
-
-

Prochains départs

-

en gare de

- - {/* Station pills: 4 grandes gares + bouton "Autre gare" */} -
- {config.stations.slice(0, 4).map((station) => { - const isSelected = selectedStation === station; - return ( - - ); - })} - - {/* Bouton "Autre gare" */} - -
- - {/* Table header */} -
- Départ - Destination - Voie - -
- - {/* Departure rows — Exclusivement BDD table "horaires" avec prise en compte de jours_personnalises et gares_desservies (quais réels) */} -
- {stationDepartures.length > 0 ? ( - stationDepartures.map((dep, dIdx) => { - return ( -
- {/* Time */} -
- - {dep.scheduledTime} - -
- - {/* Destination */} -
-
- {dep.destName} -
-
- - train - - {dep.trainLabel} -
-
- - {/* Voie / Quai réel de l'horaire pour cette gare */} - - {dep.track} - - - {/* Buy Button */} - -
- ); - }) - ) : ( -
- schedule -

- Aucun départ programmé en gare de {selectedStation} -

-

- Les horaires de cette gare seront enregistrés prochainement dans la base de données. -

-
- )} -
-
- - {/* Voir plus d'horaires link */} -
setExpandedDepartures(!expandedDepartures)} - className="flex justify-end text-[13px] font-normal text-[#222222] mt-3.5 cursor-pointer hover:underline items-center gap-1" - > - Voir plus d'horaires - chevron_right -
-
+ {/* ─── COL 1 : PROCHAINS DÉPARTS (TABLE HORAIRES SUPABASE VIA COMPOSANT RÉUTILISABLE) ─── */} + { + setCartItems(prev => [...prev, item]); + setShowCartDrawer(true); + }} + /> {/* ─── COL 2 & 3 : LES 2 DERNIÈRES ACTUALITÉS MISES EN AVANT (BDD SUPABASE) ─── */} {top2Featured.map((actu, idx) => ( @@ -1571,85 +1452,8 @@ export default function RegionContent({ {/* 7. FOOTER */} {/* ================================================================== */} - {/* Haut de page */} -
- -
- -
-
- - {/* Region selector */} -
- -
- - {/* Regional accent line */} -
- - {/* Social icons */} -
- facebook - 𝕏 - camera_alt - play_circle_filled - chat -
- - {/* Legal links */} -
-
- {["CGU/CGV Billets Régionaux", "CGU / CGV Site Internet", "Confidentialité/Cookies", "Gares", "Lignes", "ZOU !", "Accessibilité : partiellement conforme", "Mentions légales"].map((link, i, arr) => ( - - {link} - {i < arr.length - 1 && •} - - ))} -
-
- {["Plan du site", "Contacts", "Demande d'information et réclamations", "Données personnelles"].map((link, i, arr) => ( - - {link} - {i < arr.length - 1 && •} - - ))} -
-
- -
-
+ {/* REUSABLE SITE FOOTER (Image 1) */} + {/* ================================================================== */} {/* 8. MODALES & TIROIRS */} diff --git a/src/components/SiteFooter.tsx b/src/components/SiteFooter.tsx new file mode 100644 index 0000000..844787e --- /dev/null +++ b/src/components/SiteFooter.tsx @@ -0,0 +1,260 @@ +"use client"; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { getRegionTheme } from '@/lib/regionTheme'; + +interface SiteFooterProps { + regionSlug?: string; + showScrollToTop?: boolean; +} + +const REGIONS_LIST = [ + { slug: 'bourgogne-franche-comte', name: 'Bourgogne-Franche-Comté' }, + { slug: 'auvergne-rhone-alpes', name: 'Auvergne-Rhône-Alpes' }, + { slug: 'grand-est', name: 'Grand Est' }, + { slug: 'hauts-de-france', name: 'Hauts-de-France' }, + { slug: 'normandie', name: 'Normandie' }, + { slug: 'nouvelle-aquitaine', name: 'Nouvelle-Aquitaine' }, + { slug: 'occitanie', name: 'Occitanie' }, + { slug: 'pays-de-la-loire', name: 'Pays de la Loire' }, + { slug: 'centre-val-de-loire', name: 'Centre-Val de Loire' }, + { slug: 'bretagne', name: 'Bretagne' }, + { slug: 'sud-provence-alpes-cote-d-azur', name: "Provence-Alpes-Côte d'Azur" }, +]; + +export default function SiteFooter({ + regionSlug = 'bourgogne-franche-comte', + showScrollToTop = true, +}: SiteFooterProps) { + const router = useRouter(); + const [selectedRegion, setSelectedRegion] = useState(regionSlug); + const theme = getRegionTheme(selectedRegion || regionSlug); + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleRegionChange = (e: React.ChangeEvent) => { + const newSlug = e.target.value; + setSelectedRegion(newSlug); + router.push(`/${newSlug}`); + }; + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + return ( +
+ + {/* ─── 1. HAUT DE PAGE BUTTON (Centered at top) ─── */} + {showScrollToTop && ( +
+ +
+ )} + +
+ + {/* ─── 2. TOP ROW: CHANGER DE RÉGION + LOGOS PARTENAIRES ─── */} +
+ + {/* Region Switcher Select */} +
+ + + expand_more + +
+ + {/* Regional & Operator Partner Logos */} +
+ {/* SNCF Voyageurs */} +
+ Opéré par + + SNCF + + Voyageurs +
+ + {/* Region Logo badge */} +
+ + RÉGION {theme.regionName.toUpperCase()} +
+ + {/* Regional Brand */} +
+ + {theme.operatorLabel} + +
+
+
+ + {/* ─── 3. SOCIAL MEDIA ICONS (Centered) ─── */} +
+ {/* Facebook */} + + + + + + + {/* X (Twitter) */} + + + + + + + {/* Instagram */} + + + + + + + {/* YouTube */} + + + + + + + {/* WhatsApp / Message */} + + + + + +
+ + {/* ─── 4. LEGAL & NAVIGATION LINKS (Image 1) ─── */} +
+ + {/* First Line */} +
+ + CGU/CGV TRAIN {theme.operatorLabel} + + • + + CGU / CGV Site Internet + + • + + Confidentialité/Cookies + + • + + Gares + + • + + Lignes + + • + + {theme.operatorLabel} + + • + + Accessibilité : partiellement conforme + + • + + Mentions légales + + • + + Plan du site + + • + + Contacts + +
+ + {/* Second Line */} +
+ + Demande d'information et réclamations + + • + + Données personnelles + +
+ +
+ © FERROVIA - Tous droits réservés. +
+ +
+ +
+
+ ); +} diff --git a/src/components/StationDeparturesBoard.tsx b/src/components/StationDeparturesBoard.tsx new file mode 100644 index 0000000..416f8bf --- /dev/null +++ b/src/components/StationDeparturesBoard.tsx @@ -0,0 +1,344 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from 'react'; +import Link from 'next/link'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; + +export interface ResolvedDeparture { + id: string | number; + scheduledTime: string; + destName: string; + trainType: string; + trainNumber: string; + trainLabel: string; + track: string; + rawQuai?: string | null; + sourceHoraire?: any; +} + +interface StationDeparturesBoardProps { + regionSlug?: string; + initialStation?: string; + showStationSelector?: boolean; + availableStations?: string[]; + onStationChange?: (station: string) => void; + onAddToCart?: (item: { id: number; title: string; price: number; date: string }) => void; + className?: string; +} + +export default function StationDeparturesBoard({ + regionSlug = "bourgogne-franche-comte", + initialStation = "Dijon", + showStationSelector = true, + availableStations, + onStationChange, + onAddToCart, + className = "", +}: StationDeparturesBoardProps) { + const theme = getRegionTheme(regionSlug); + + const [selectedStation, setSelectedStation] = useState(initialStation); + const [horairesDataList, setHorairesDataList] = useState([]); + const [garesTypeMap, setGaresTypeMap] = useState>({}); + const [expandedDepartures, setExpandedDepartures] = useState(false); + + // Sync station if prop changes + useEffect(() => { + if (initialStation) { + setSelectedStation(initialStation); + } + }, [initialStation]); + + // Fetch gares and horaires + useEffect(() => { + const fetchBoardData = async () => { + try { + const { data: gData } = await supabase + .from('gares') + .select('id, nom, type') + .is('deleted_at', null); + + const gMap: Record = {}; + const gTypeMap: Record = {}; + if (gData) { + gData.forEach((g: any) => { + gMap[g.id] = g.nom; + gTypeMap[g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim()] = g.type || 'ville'; + }); + setGaresTypeMap(gTypeMap); + } + + const { data: hData } = await supabase + .from('horaires') + .select('*') + .eq('actif', true) + .is('deleted_at', null) + .order('heure_depart', { ascending: true }); + + if (hData) { + setHorairesDataList(hData.map((h: any) => ({ + ...h, + gare_depart_nom: gMap[h.gare_depart_id] || '', + gare_arrivee_nom: gMap[h.gare_arrivee_id] || '', + }))); + } + } catch (err) { + console.error('Error fetching data for StationDeparturesBoard:', err); + } + }; + + fetchBoardData(); + }, [regionSlug]); + + const normalizeStationName = (s: string) => (s || '').toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + + const getLocalDateStr = (d: Date = new Date()) => { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + const isTrainCirculating = (h: any, targetDate: string = getLocalDateStr()) => { + if (h.actif === false) return false; + if (Array.isArray(h.jours_personnalises) && h.jours_personnalises.length > 0) { + const customDay = h.jours_personnalises.find((jp: any) => { + const d = jp.date || jp.jour || ''; + return d === targetDate || d.slice(0, 10) === targetDate; + }); + if (customDay) return customDay.circule === true; + } + return false; + }; + + // Filter and resolve departures for selectedStation + const stationDepartures: ResolvedDeparture[] = useMemo(() => { + if (!selectedStation || horairesDataList.length === 0) return []; + + const selectedStationNorm = normalizeStationName(selectedStation); + const todayDateStr = getLocalDateStr(); + const results: ResolvedDeparture[] = []; + + for (const h of horairesDataList) { + if (!isTrainCirculating(h, todayDateStr)) continue; + + let gares: any[] = []; + if (typeof h.gares_desservies === 'string') { + try { gares = JSON.parse(h.gares_desservies); } catch (e) { gares = []; } + } else if (Array.isArray(h.gares_desservies)) { + gares = h.gares_desservies; + } + + const stopIndex = gares.findIndex(g => { + const gNomNorm = normalizeStationName(g.gare_nom || ''); + return gNomNorm === selectedStationNorm || + gNomNorm.includes(selectedStationNorm) || + selectedStationNorm.includes(gNomNorm); + }); + + if (stopIndex !== -1) { + const stop = gares[stopIndex]; + const isTerminus = stop.is_arrivee || (stopIndex === gares.length - 1 && !stop.heure_depart) || !stop.heure_depart; + + if (!isTerminus) { + const lastStop = gares[gares.length - 1]; + const destination = h.gare_arrivee_nom || lastStop?.gare_nom || "Terminus"; + const departureTime = (stop.heure_depart || '').slice(0, 5); + + // Track display logic + const currentStationType = garesTypeMap[selectedStationNorm] || 'ville'; + const now = new Date(); + const [depH, depM] = departureTime.split(':').map(Number); + const depMinutes = (isNaN(depH) ? 0 : depH) * 60 + (isNaN(depM) ? 0 : depM); + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const diffMinutes = depMinutes - currentMinutes; + + let displayQuai = '-'; + if (stop.quai && stop.quai.trim() !== '') { + if (currentStationType === 'ville') { + if (diffMinutes <= 30) displayQuai = stop.quai; + } else { + if (diffMinutes <= 360) displayQuai = stop.quai; + } + } + if (displayQuai === '-' && stop.quai) { + displayQuai = stop.quai; + } + + results.push({ + id: `${h.id}-${stopIndex}`, + scheduledTime: departureTime, + destName: destination, + trainType: h.type_train || 'TRAIN MOBIGO', + trainNumber: h.numero_train || '', + trainLabel: `${h.type_train?.toUpperCase() || theme.operatorLabel || 'TER'} ${h.numero_train || ''}`.trim(), + track: displayQuai, + rawQuai: stop.quai, + sourceHoraire: h, + }); + } + } + } + + return results.sort((a, b) => a.scheduledTime.localeCompare(b.scheduledTime)); + }, [selectedStation, horairesDataList, garesTypeMap, theme.operatorLabel]); + + const defaultPillStations = availableStations || [ + "Dijon", "Besançon Viotte", "Belfort", "Chalon-sur-Saône" + ]; + + const handleSelectStation = (st: string) => { + setSelectedStation(st); + if (onStationChange) onStationChange(st); + }; + + const getRegionPath = (path: string) => { + return regionSlug ? `/${regionSlug}${path}` : path; + }; + + return ( +
+
+

Prochains départs

+

en gare de

+ + {/* Station pills (if selector enabled) */} + {showStationSelector && ( +
+ {defaultPillStations.slice(0, 4).map((st) => { + const isSelected = selectedStation.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim() === st.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return ( + + ); + })} + + + Autre gare + chevron_right + +
+ )} + + {/* Table header */} +
+ Départ + Destination + Voie + +
+ + {/* Departure rows */} +
+ {stationDepartures.length > 0 ? ( + stationDepartures.map((dep, dIdx) => ( +
+ {/* Time */} +
+ + {dep.scheduledTime} + +
+ + {/* Destination */} +
+
+ {dep.destName} +
+
+ + train + + {dep.trainLabel} +
+
+ + {/* Voie / Quai */} + + {dep.track} + + + {/* Buy / Action Button */} + +
+ )) + ) : ( +
+ schedule +

+ Aucun départ programmé en gare de {selectedStation} +

+

+ Les horaires de cette gare seront enregistrés prochainement dans la base de données. +

+
+ )} +
+
+ + {/* Voir plus d'horaires link */} +
+ + + Voir plus d'horaires + chevron_right + +
+
+ ); +} diff --git a/src/components/TimetableSearchForm.tsx b/src/components/TimetableSearchForm.tsx index 4479a97..751b960 100644 --- a/src/components/TimetableSearchForm.tsx +++ b/src/components/TimetableSearchForm.tsx @@ -1,8 +1,9 @@ "use client"; import React, { useState, useEffect, useRef } from 'react'; -import { useRouter, usePathname } from 'next/navigation'; +import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; type ActiveField = 'departure' | 'arrival' | 'via' | 'outwardDate' | 'outwardTime' | 'returnDate' | 'returnTime' | 'passenger' | null; @@ -87,6 +88,7 @@ export default function TimetableSearchForm({ }: TimetableSearchFormProps) { const router = useRouter(); const pathname = usePathname(); + const searchParams = useSearchParams(); const containerRef = useRef(null); // Determine current region slug from props or URL pathname @@ -100,6 +102,8 @@ export default function TimetableSearchForm({ return ''; })(); + const theme = getRegionTheme(currentRegionSlug); + // Form states const [departureStation, setDepartureStation] = useState(initialDeparture); const [arrivalStation, setArrivalStation] = useState(initialArrival); @@ -126,6 +130,76 @@ export default function TimetableSearchForm({ { id: 1, age: '', discountCard: 'Carte de réduction (optionnel)' } ]); + // Priority: URL searchParams > props > sessionStorage > default + useEffect(() => { + // 1. Try from URL search params + const paramDep = searchParams?.get('departure'); + const paramArr = searchParams?.get('arrival'); + const paramVia = searchParams?.get('via'); + const paramOutDate = searchParams?.get('outwardDate'); + const paramOutTime = searchParams?.get('outwardTime'); + const paramHasReturn = searchParams?.get('hasReturn'); + const paramRetDate = searchParams?.get('returnDate'); + const paramRetTime = searchParams?.get('returnTime'); + const paramPassCount = searchParams?.get('passengers'); + const paramPassAge = searchParams?.get('passengerAge'); + const paramDiscount = searchParams?.get('discountCard'); + + if (paramDep || paramArr || paramOutDate) { + if (paramDep) setDepartureStation(paramDep); + if (paramArr) setArrivalStation(paramArr); + if (paramVia) { + setViaStation(paramVia); + setShowVia(true); + } + if (paramOutDate) setOutwardDate(paramOutDate); + if (paramOutTime) setOutwardTime(paramOutTime); + if (paramHasReturn === '1' || (paramRetDate && paramRetDate !== '')) { + setHasReturn(true); + if (paramRetDate) setReturnDate(paramRetDate); + if (paramRetTime) setReturnTime(paramRetTime); + } + const count = parseInt(paramPassCount || '1', 10); + const newPassengers: PassengerInfo[] = []; + const total = isNaN(count) || count < 1 ? 1 : count; + for (let i = 0; i < total; i++) { + newPassengers.push({ + id: i + 1, + age: i === 0 ? (paramPassAge || '') : '', + discountCard: i === 0 ? (paramDiscount || 'Carte de réduction (optionnel)') : 'Carte de réduction (optionnel)', + }); + } + setPassengers(newPassengers); + return; + } + + // 2. Try from sessionStorage if available + try { + const stored = sessionStorage.getItem('ferrovia_last_search'); + if (stored) { + const parsed = JSON.parse(stored); + if (parsed.departure) setDepartureStation(parsed.departure); + if (parsed.arrival) setArrivalStation(parsed.arrival); + if (parsed.via) { + setViaStation(parsed.via); + setShowVia(true); + } + if (parsed.outwardDate) setOutwardDate(parsed.outwardDate); + if (parsed.outwardTime) setOutwardTime(parsed.outwardTime); + if (parsed.hasReturn) { + setHasReturn(true); + if (parsed.returnDate) setReturnDate(parsed.returnDate); + if (parsed.returnTime) setReturnTime(parsed.returnTime); + } + if (parsed.passengers && Array.isArray(parsed.passengers) && parsed.passengers.length > 0) { + setPassengers(parsed.passengers); + } + } + } catch (e) { + // ignore + } + }, [searchParams]); + // Fetch stations from Supabase useEffect(() => { const fetchGares = async () => { @@ -186,6 +260,26 @@ export default function TimetableSearchForm({ ? firstPassenger.discountCard : 'Sans carte'; + const searchData = { + departure: departureStation || 'Dijon', + arrival: arrivalStation || 'Seurre', + via: showVia ? viaStation : '', + outwardDate, + outwardTime, + hasReturn, + returnDate: hasReturn ? returnDate : '', + returnTime: hasReturn ? returnTime : '', + passengers, + passengerCount: passengers.length, + discountCard: discountStr, + }; + + try { + sessionStorage.setItem('ferrovia_last_search', JSON.stringify(searchData)); + } catch (err) { + // ignore + } + if (onSearch) { onSearch({ departure: departureStation || 'Dijon', @@ -311,14 +405,15 @@ export default function TimetableSearchForm({ {/* 1. Gare de départ */}
setActiveField('departure')} + style={activeField === 'departure' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined} className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${ activeField === 'departure' - ? 'bg-[#fff8f8] border-2 border-dashed border-[#c00] shadow-xs' + ? 'border-2 border-dashed shadow-xs' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' }`} >
- + place swap_vert @@ -346,13 +442,14 @@ export default function TimetableSearchForm({ {/* 2. Gare d'arrivée */}
setActiveField('arrival')} + style={activeField === 'arrival' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined} className={`rounded-xl px-4 py-3 flex items-center gap-3 transition-all cursor-pointer ${ activeField === 'arrival' - ? 'bg-[#fff8f8] border-2 border-dashed border-[#c00] shadow-xs' + ? 'border-2 border-dashed shadow-xs' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' }`} > - + place {showVia ? "- Supprimer Via" : "+ Via"} @@ -385,9 +483,10 @@ export default function TimetableSearchForm({ {showVia && (
setActiveField('via')} + style={activeField === 'via' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined} className={`rounded-xl px-4 py-2.5 flex items-center gap-2.5 transition-all cursor-pointer ${ activeField === 'via' - ? 'bg-[#fff8f8] border-2 border-dashed border-[#c00]' + ? 'border-2 border-dashed' : 'bg-[#f2f3f5] hover:bg-[#eaecef]' }`} > @@ -405,9 +504,10 @@ export default function TimetableSearchForm({ {/* 3. Aller */}
@@ -415,7 +515,7 @@ export default function TimetableSearchForm({ onClick={() => setActiveField('outwardDate')} className="flex items-center gap-3 flex-1 cursor-pointer" > - + event
@@ -430,7 +530,7 @@ export default function TimetableSearchForm({
setActiveField('outwardTime')} - className="w-14 text-left font-normal text-[14px] text-gray-900 cursor-pointer hover:text-[#006837]" + className="w-14 text-left font-normal text-[14px] text-gray-900 cursor-pointer hover:opacity-80" > {outwardTime}
@@ -438,9 +538,10 @@ export default function TimetableSearchForm({ {/* 4. Retour */}
@@ -451,7 +552,7 @@ export default function TimetableSearchForm({ }} className="flex items-center gap-3 flex-1 cursor-pointer" > - + event @@ -467,7 +568,7 @@ export default function TimetableSearchForm({ setHasReturn(true); setActiveField('returnTime'); }} - className="w-14 text-left font-normal text-[13.5px] text-gray-700 cursor-pointer hover:text-[#006837]" + className="w-14 text-left font-normal text-[13.5px] text-gray-700 cursor-pointer hover:opacity-80" > {hasReturn ? returnTime : "--:--"}
@@ -496,14 +597,15 @@ export default function TimetableSearchForm({ {/* 5. Voyageurs (CONFORME IMG 1) */}
setActiveField('passenger')} + style={activeField === 'passenger' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined} className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${ activeField === 'passenger' - ? 'bg-[#f7faef] border-2 border-dashed border-[#006837] shadow-xs' + ? 'border-2 border-dashed shadow-xs' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' }`} >
- + person @@ -512,7 +614,7 @@ export default function TimetableSearchForm({ {passengers[0]?.discountCard && passengers[0].discountCard !== 'Carte de réduction (optionnel)' ? `, ${passengers[0].discountCard}` : ', sans carte'}
- + add
@@ -521,7 +623,8 @@ export default function TimetableSearchForm({
@@ -571,10 +674,16 @@ export default function TimetableSearchForm({
handleSelectStation(st)} + style={isFirst ? { + backgroundColor: theme.primaryLight, + borderColor: theme.primary, + } : { + borderColor: `${theme.primary}25`, + }} className={`transition-all cursor-pointer ${ isFirst - ? 'bg-[#e6f0d7] border border-dashed border-[#006837] rounded-xl p-3 text-[15px] font-medium text-gray-900 shadow-2xs' - : 'py-2.5 px-2 border-b border-[#006837] text-[15px] text-gray-900 hover:bg-[#f4f8ec] transition-colors font-medium' + ? 'border border-dashed rounded-xl p-3 text-[15px] font-medium text-gray-900 shadow-2xs' + : 'py-2.5 px-2 border-b text-[15px] text-gray-900 hover:opacity-80 transition-colors font-medium' }`} > {st} @@ -655,11 +764,12 @@ export default function TimetableSearchForm({ type="button" disabled={isPastDate} onClick={() => !isPastDate && handleSelectDay(dayNum)} + style={isSelected ? { backgroundColor: theme.secondary || theme.primary, color: '#111' } : undefined} className={`h-9 w-9 mx-auto rounded-full flex items-center justify-center font-normal transition-all ${ isPastDate ? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50' : isSelected - ? 'bg-[#c4d600] text-gray-900 font-bold shadow-xs scale-105' + ? 'font-bold shadow-xs scale-105' : 'hover:bg-gray-100 text-gray-800 cursor-pointer' }`} title={isPastDate ? 'Date passée' : undefined} @@ -693,12 +803,19 @@ export default function TimetableSearchForm({
!isPastTime && handleSelectTime(slot)} + style={isSelected ? { + backgroundColor: theme.primaryLight, + color: theme.primary, + borderColor: theme.primary, + } : { + borderColor: `${theme.primary}25`, + }} className={`py-2.5 px-4 text-[15px] transition-colors ${ isPastTime ? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50 line-through border-b border-gray-100' : isSelected - ? 'bg-[#e5ecc5] font-semibold text-gray-900 cursor-pointer' - : 'border-b border-[#006837] text-gray-900 hover:bg-[#f4f8ec] cursor-pointer' + ? 'font-bold cursor-pointer border-b' + : 'border-b text-gray-900 hover:opacity-80 cursor-pointer' }`} title={isPastTime ? 'Heure passée' : undefined} > @@ -722,7 +839,10 @@ export default function TimetableSearchForm({
- + person @@ -740,8 +860,11 @@ export default function TimetableSearchForm({ )}
- {/* Field 1: Âge (optionnel) - Box with dashed green border matching Image 1 */} -
+ {/* Field 1: Âge (optionnel) - Box with dashed theme border */} +
@@ -768,7 +891,7 @@ export default function TimetableSearchForm({ onChange={(e) => handleUpdatePassenger(p.id, 'discountCard', e.target.value)} className="w-full bg-transparent text-[13px] font-medium text-gray-800 outline-none cursor-pointer appearance-none pr-6" style={{ - backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23006837' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E")`, + backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='${encodeURIComponent(theme.primary)}' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E")`, backgroundRepeat: 'no-repeat', backgroundPosition: 'right 0 center', backgroundSize: '20px' @@ -783,15 +906,19 @@ export default function TimetableSearchForm({ ))}
- {/* Yellow Horizontal Divider Line (CONFORME IMG 1) */} -
+ {/* Theme Divider Line */} +
- {/* + Ajouter un voyageur Green Button (CONFORME IMG 1) */} + {/* + Ajouter un voyageur Button */}
); } - diff --git a/src/lib/regionTheme.ts b/src/lib/regionTheme.ts index 249ba4d..0e6318f 100644 --- a/src/lib/regionTheme.ts +++ b/src/lib/regionTheme.ts @@ -3,93 +3,156 @@ export interface RegionTheme { primaryHover: string; primaryLight: string; secondary: string; + bannerBg: string; badgeText: string; + operatorLabel: string; + regionName: string; + heroImage: string; } export const REGION_THEMES: Record = { 'bourgogne-franche-comte': { primary: '#006837', primaryHover: '#00522b', - primaryLight: '#eef5e6', - secondary: '#8da300', + primaryLight: '#edf4d2', + secondary: '#8ba81b', + bannerBg: '#8ba81b', badgeText: 'TER Bourgogne-Franche-Comté', + operatorLabel: 'MOBIGO', + regionName: 'Bourgogne-Franche-Comté', + heroImage: 'https://images.unsplash.com/photo-1500382017468-9049fed747ef?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'occitanie': { primary: '#c0001a', primaryHover: '#990014', primaryLight: '#fde8ea', secondary: '#e60000', + bannerBg: '#c0001a', badgeText: 'liO Train Occitanie', + operatorLabel: 'liO', + regionName: 'Occitanie', + heroImage: 'https://images.unsplash.com/photo-1544816155-12df9643f363?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'provence-alpes-cote-azur': { primary: '#005088', primaryHover: '#003a63', primaryLight: '#e5f0f8', - secondary: '#009fe3', + secondary: '#ffd100', + bannerBg: '#005088', badgeText: 'ZOU ! TER PACA', + operatorLabel: 'ZOU !', + regionName: "Provence-Alpes-Côte d'Azur", + heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'sud-provence-alpes-cote-d-azur': { primary: '#005088', primaryHover: '#003a63', primaryLight: '#e5f0f8', - secondary: '#009fe3', + secondary: '#ffd100', + bannerBg: '#005088', badgeText: 'ZOU ! TER PACA', + operatorLabel: 'ZOU !', + regionName: "Provence-Alpes-Côte d'Azur", + heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'bretagne': { primary: '#0077b6', primaryHover: '#005b8c', primaryLight: '#e6f4fa', secondary: '#0096c7', + bannerBg: '#0077b6', badgeText: 'TER BreizhGo', + operatorLabel: 'BreizhGo', + regionName: 'Bretagne', + heroImage: 'https://images.unsplash.com/photo-1506929562872-bb421503ef21?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'grand-est': { - primary: '#2b5c8f', - primaryHover: '#1e4267', - primaryLight: '#eaf0f6', + primary: '#0082c8', + primaryHover: '#00629b', + primaryLight: '#eaf4fb', secondary: '#4885c1', + bannerBg: '#0082c8', badgeText: 'TER Fluo Grand Est', + operatorLabel: 'Fluo Grand Est', + regionName: 'Grand Est', + heroImage: 'https://images.unsplash.com/photo-1518684079-3c830dcef090?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'normandie': { primary: '#d32f2f', primaryHover: '#b71c1c', primaryLight: '#fbe9e7', - secondary: '#e53935', + secondary: '#005c94', + bannerBg: '#005c94', badgeText: 'TER Nomad Normandie', + operatorLabel: 'NOMAD', + regionName: 'Normandie', + heroImage: 'https://images.unsplash.com/photo-1509316975850-ff9c5deb0cd9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'hauts-de-france': { primary: '#004b87', primaryHover: '#003663', primaryLight: '#e5edf5', secondary: '#0066b3', + bannerBg: '#004b87', badgeText: 'TER Hauts-de-France', + operatorLabel: 'Hauts-de-France', + regionName: 'Hauts-de-France', + heroImage: 'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'pays-de-la-loire': { primary: '#008459', primaryHover: '#006343', primaryLight: '#e6f5ef', secondary: '#00a872', + bannerBg: '#008459', badgeText: 'TER Aléop', + operatorLabel: 'Aléop', + regionName: 'Pays de la Loire', + heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'nouvelle-aquitaine': { - primary: '#0066a4', - primaryHover: '#004c7a', - primaryLight: '#e6f2fa', - secondary: '#0085d0', + primary: '#008542', + primaryHover: '#006633', + primaryLight: '#e6f5ef', + secondary: '#8ba81b', + bannerBg: '#008542', badgeText: 'TER Nouvelle-Aquitaine', + operatorLabel: 'Nouvelle-Aquitaine', + regionName: 'Nouvelle-Aquitaine', + heroImage: 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'centre-val-de-loire': { - primary: '#008080', - primaryHover: '#006666', - primaryLight: '#e6f2f2', - secondary: '#00a3a3', + primary: '#e2001a', + primaryHover: '#b50015', + primaryLight: '#fde8ea', + secondary: '#008080', + bannerBg: '#e2001a', badgeText: 'TER Rémi', + operatorLabel: 'Rémi', + regionName: 'Centre-Val de Loire', + heroImage: 'https://images.unsplash.com/photo-1516483638261-f4dbaf036963?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, 'auvergne-rhone-alpes': { primary: '#00609c', primaryHover: '#004875', primaryLight: '#e6f1f8', secondary: '#007bc7', + bannerBg: '#00609c', badgeText: 'TER Auvergne-Rhône-Alpes', + operatorLabel: 'AURA', + regionName: 'Auvergne-Rhône-Alpes', + heroImage: 'https://images.unsplash.com/photo-1530122037265-a5f1f91d3b99?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', + }, + 'ile-de-france': { + primary: '#0055a5', + primaryHover: '#003e7a', + primaryLight: '#e5f0fa', + secondary: '#009fe3', + bannerBg: '#0055a5', + badgeText: 'Transilien SNCF', + operatorLabel: 'Transilien', + regionName: 'Île-de-France', + heroImage: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', }, };