From 24cb50277083e7da648f38d8a71a52d4e0cca8de Mon Sep 17 00:00:00 2001 From: mathi Date: Wed, 23 Sep 2026 17:43:55 +0200 Subject: [PATCH] =?UTF-8?q?Impl=C3=A9mentation=20des=20page=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -> Prochains départs / Arrivées (beta) -> Recherches d'horaires (actif) --- public/images/train_agc.png | Bin 0 -> 77141 bytes src/app/[region]/horaires/page.tsx | 79 + src/app/[region]/offers/page.tsx | 52 + src/app/[region]/page.tsx | 63 +- .../[region]/se-deplacer/horaires/page.tsx | 13 + .../se-deplacer/prochains-departs/page.tsx | 49 + src/app/horaires/page.tsx | 63 + src/app/offers/page.tsx | 44 + .../se-deplacer/prochains-departs/page.tsx | 41 + src/components/HeaderMenu.tsx | 394 ++++ src/components/HorairesPageClient.tsx | 172 ++ src/components/OffersPageClient.tsx | 1140 ++++++++++ src/components/ProchainsDepartsClient.tsx | 1865 +++++++++++++++++ src/components/RegionContent.tsx | 676 ++---- src/components/SiteHeader.tsx | 297 +++ src/components/TimetableSearchForm.tsx | 810 +++++++ src/lib/perturbationUtils.ts | 113 + src/lib/regionTheme.ts | 99 + 18 files changed, 5500 insertions(+), 470 deletions(-) create mode 100644 public/images/train_agc.png create mode 100644 src/app/[region]/horaires/page.tsx create mode 100644 src/app/[region]/offers/page.tsx create mode 100644 src/app/[region]/se-deplacer/horaires/page.tsx create mode 100644 src/app/[region]/se-deplacer/prochains-departs/page.tsx create mode 100644 src/app/horaires/page.tsx create mode 100644 src/app/offers/page.tsx create mode 100644 src/app/se-deplacer/prochains-departs/page.tsx create mode 100644 src/components/HeaderMenu.tsx create mode 100644 src/components/HorairesPageClient.tsx create mode 100644 src/components/OffersPageClient.tsx create mode 100644 src/components/ProchainsDepartsClient.tsx create mode 100644 src/components/SiteHeader.tsx create mode 100644 src/components/TimetableSearchForm.tsx create mode 100644 src/lib/perturbationUtils.ts create mode 100644 src/lib/regionTheme.ts diff --git a/public/images/train_agc.png b/public/images/train_agc.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1bc87387ae8e452a4bed46165d41fef81c6db8 GIT binary patch literal 77141 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

te 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 X-^gX; +}) { + const { region } = await params; + + const regionCandidates = Array.from(new Set([ + region, + region === 'sud-provence-alpes-cote-d-azur' ? 'provence-alpes-cote-azur' : '', + region === 'provence-alpes-cote-azur' ? 'sud-provence-alpes-cote-d-azur' : '', + ].filter(Boolean))); + + // Fetch parameters + const { data } = await supabase + .from('parametres') + .select('valeur') + .eq('cle', 'general') + .single(); + + let regionData = { + nom_site: `FERROVIA ${region.replace(/-/g, ' ').toUpperCase()}`, + logo_header_url: '', + description_site: '', + }; + + if (data && data.valeur) { + const val = data.valeur as any; + let matchedRegionKey = regionCandidates.find(k => val.regions && val.regions[k]) || regionCandidates[0]; + const rObj = val.regions?.[matchedRegionKey] || {}; + + const logoUrl = rObj.logo_header_url + || val.logo_header_url?.[matchedRegionKey] + || val.logo_header_url_by_region?.[matchedRegionKey] + || val.logo_header_by_region?.[matchedRegionKey] + || ''; + + const siteName = rObj.nom_site + || val.nom_site_by_region?.[matchedRegionKey] + || `FERROVIA ${region.replace(/-/g, ' ').toUpperCase()}`; + + regionData = { + nom_site: siteName, + logo_header_url: logoUrl, + description_site: rObj.description_site || '', + }; + } + + // Fetch menus_regionaux + let menuStructure: any = null; + try { + const { data: menuData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .limit(1); + if (menuData && menuData.length > 0) { + menuStructure = menuData[0].structure; + } + } catch (e) { + console.error('Error fetching menus_regionaux:', e); + } + + return ( + + ); +} diff --git a/src/app/[region]/offers/page.tsx b/src/app/[region]/offers/page.tsx new file mode 100644 index 0000000..83b5460 --- /dev/null +++ b/src/app/[region]/offers/page.tsx @@ -0,0 +1,52 @@ +import React, { Suspense } from 'react'; +import { notFound } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import OffersPageClient from '@/components/OffersPageClient'; + +interface RegionOffersPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionOffersPage({ params }: RegionOffersPageProps) { + const { region } = await params; + let menuStructure = null; + + try { + const { data: regionData } = await supabase + .from('regions') + .select('id, nom') + .eq('code_slug', region) + .maybeSingle(); + + if (regionData?.id) { + const { data: menuData } = await supabase + .from('menus_regionaux') + .select('structure') + .eq('region_id', regionData.id) + .maybeSingle(); + + if (menuData?.structure) { + menuStructure = typeof menuData.structure === 'string' + ? JSON.parse(menuData.structure) + : menuData.structure; + } + } + } catch (err) { + console.error(`Error loading menu for region ${region}:`, err); + } + + return ( + + Chargement des offres de billets... + + }> + + + ); +} diff --git a/src/app/[region]/page.tsx b/src/app/[region]/page.tsx index 12cc023..2db75fa 100644 --- a/src/app/[region]/page.tsx +++ b/src/app/[region]/page.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { supabase } from '@/lib/supabase'; -import RegionContent, { InfoTraficItem, ActualiteItem, HoraireItem } from '@/components/RegionContent'; +import RegionContent, { InfoTraficItem, ActualiteItem, EvenementItem, HoraireItem } from '@/components/RegionContent'; interface RegionData { nom_site: string; @@ -18,6 +18,13 @@ export default async function RegionPage({ }) { const { region } = await params; + // Region candidates for alias support + const regionCandidates = Array.from(new Set([ + region, + region === 'sud-provence-alpes-cote-d-azur' ? 'provence-alpes-cote-azur' : '', + region === 'provence-alpes-cote-azur' ? 'sud-provence-alpes-cote-d-azur' : '', + ].filter(Boolean))); + // 1. Fetch general settings const { data } = await supabase .from('parametres') @@ -33,15 +40,7 @@ export default async function RegionPage({ if (data && data.valeur) { const val = data.valeur as any; - - // Check direct region key or aliases (e.g. sud-provence-alpes-cote-d-azur <-> provence-alpes-cote-azur) - const candidates = [ - region, - region === 'sud-provence-alpes-cote-d-azur' ? 'provence-alpes-cote-azur' : '', - region === 'provence-alpes-cote-azur' ? 'sud-provence-alpes-cote-d-azur' : '', - ].filter(Boolean); - - let matchedRegionKey = candidates.find(k => val.regions && val.regions[k]) || candidates[0]; + let matchedRegionKey = regionCandidates.find(k => val.regions && val.regions[k]) || regionCandidates[0]; const rObj = val.regions?.[matchedRegionKey] || {}; @@ -66,13 +65,14 @@ export default async function RegionPage({ }; } - // 2. Fetch info trafic from Supabase table "info_trafic" + // 2. Fetch info trafic strictly filtered by region_id let infoTraficList: InfoTraficItem[] = []; try { const { data: trafficData } = await supabase .from('info_trafic') .select('*') .eq('actif', true) + .in('region_id', regionCandidates) .is('deleted_at', null) .order('created_at', { ascending: false }); if (trafficData) { @@ -82,13 +82,14 @@ export default async function RegionPage({ console.error('Error fetching info_trafic:', e); } - // 3. Fetch actualites from Supabase table "actualites" + // 3. Fetch actualites strictly filtered by region_id let actualitesList: ActualiteItem[] = []; try { const { data: actusData } = await supabase .from('actualites') .select('*') .eq('actif', true) + .in('region_id', regionCandidates) .is('deleted_at', null) .order('date_publication', { ascending: false }) .order('created_at', { ascending: false }); @@ -99,7 +100,40 @@ export default async function RegionPage({ console.error('Error fetching actualites:', e); } - // 4. Fetch gares and horaires from Supabase table "horaires" + // 4. Fetch evenements strictly filtered by region_id + let evenementsList: EvenementItem[] = []; + try { + const { data: eventsData } = await supabase + .from('evenements') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .is('deleted_at', null) + .order('date_debut', { ascending: false }); + if (eventsData) { + evenementsList = eventsData as EvenementItem[]; + } + } catch (e) { + console.error('Error fetching evenements:', e); + } + + // 5. Fetch menus_regionaux strictly filtered by region_id + let menuStructure: any = null; + try { + const { data: menuData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .limit(1); + if (menuData && menuData.length > 0) { + menuStructure = menuData[0].structure; + } + } catch (e) { + console.error('Error fetching menus_regionaux:', e); + } + + // 6. Fetch gares and horaires strictly filtered by region_id let horairesList: HoraireItem[] = []; try { const { data: garesData } = await supabase @@ -137,8 +171,9 @@ export default async function RegionPage({ regionData={regionData} initialInfoTrafic={infoTraficList} initialActualites={actualitesList} + initialEvenements={evenementsList} + initialMenuStructure={menuStructure} initialHoraires={horairesList} /> ); } - diff --git a/src/app/[region]/se-deplacer/horaires/page.tsx b/src/app/[region]/se-deplacer/horaires/page.tsx new file mode 100644 index 0000000..52d30e3 --- /dev/null +++ b/src/app/[region]/se-deplacer/horaires/page.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import RegionHorairesPage from '../../horaires/page'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +export default async function SeDeplacerHorairesPage({ + params, +}: { + params: Promise<{ region: string }>; +}) { + return ; +} diff --git a/src/app/[region]/se-deplacer/prochains-departs/page.tsx b/src/app/[region]/se-deplacer/prochains-departs/page.tsx new file mode 100644 index 0000000..e8c7fad --- /dev/null +++ b/src/app/[region]/se-deplacer/prochains-departs/page.tsx @@ -0,0 +1,49 @@ +import React, { Suspense } from 'react'; +import { supabase } from '@/lib/supabase'; +import ProchainsDepartsClient from '@/components/ProchainsDepartsClient'; + +interface RegionProchainsDepartsPageProps { + params: Promise<{ + region: string; + }>; +} + +export default async function RegionProchainsDepartsPage({ params }: RegionProchainsDepartsPageProps) { + const { region } = await params; + let menuStructure = null; + + try { + const { data: regionData } = await supabase + .from('regions') + .select('id, nom') + .or(`slug.eq.${region},code.eq.${region}`); + + const rIds = regionData ? regionData.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: menuData } = await query.limit(1); + + if (menuData && menuData.length > 0 && Array.isArray(menuData[0].structure)) { + menuStructure = menuData[0].structure; + } + } catch (err) { + console.error(`Error loading menu for region ${region}:`, err); + } + + return ( + + Chargement des prochains départs... + + }> + + + ); +} diff --git a/src/app/horaires/page.tsx b/src/app/horaires/page.tsx new file mode 100644 index 0000000..5688d64 --- /dev/null +++ b/src/app/horaires/page.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { supabase } from '@/lib/supabase'; +import HorairesPageClient from '@/components/HorairesPageClient'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +export default async function GlobalHorairesPage() { + const { data } = await supabase + .from('parametres') + .select('valeur') + .eq('cle', 'general') + .single(); + + let targetRegion = 'bourgogne-franche-comte'; + if (data && data.valeur) { + const val = data.valeur as any; + if (val.region) { + targetRegion = val.region; + } + } + + const regionCandidates = [targetRegion]; + + let regionData = { + nom_site: `FERROVIA ${targetRegion.replace(/-/g, ' ').toUpperCase()}`, + logo_header_url: '', + description_site: '', + }; + + if (data && data.valeur) { + const val = data.valeur as any; + const rObj = val.regions?.[targetRegion] || {}; + regionData = { + nom_site: rObj.nom_site || `FERROVIA ${targetRegion.replace(/-/g, ' ').toUpperCase()}`, + logo_header_url: rObj.logo_header_url || val.logo_header_url?.[targetRegion] || '', + description_site: rObj.description_site || '', + }; + } + + let menuStructure: any = null; + try { + const { data: menuData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .limit(1); + if (menuData && menuData.length > 0) { + menuStructure = menuData[0].structure; + } + } catch (e) { + console.error('Error fetching menus_regionaux:', e); + } + + return ( + + ); +} diff --git a/src/app/offers/page.tsx b/src/app/offers/page.tsx new file mode 100644 index 0000000..fea1ce8 --- /dev/null +++ b/src/app/offers/page.tsx @@ -0,0 +1,44 @@ +import React, { Suspense } from 'react'; +import { supabase } from '@/lib/supabase'; +import OffersPageClient from '@/components/OffersPageClient'; + +export default async function OffersPage() { + let menuStructure = null; + + try { + const { data: regionData } = await supabase + .from('regions') + .select('id') + .eq('code_slug', 'bourgogne-franche-comte') + .maybeSingle(); + + if (regionData?.id) { + const { data: menuData } = await supabase + .from('menus_regionaux') + .select('structure') + .eq('region_id', regionData.id) + .maybeSingle(); + + if (menuData?.structure) { + menuStructure = typeof menuData.structure === 'string' + ? JSON.parse(menuData.structure) + : menuData.structure; + } + } + } catch (err) { + console.error('Error loading menu for offers:', err); + } + + return ( + + Chargement des offres de billets... + + }> + + + ); +} diff --git a/src/app/se-deplacer/prochains-departs/page.tsx b/src/app/se-deplacer/prochains-departs/page.tsx new file mode 100644 index 0000000..d47e446 --- /dev/null +++ b/src/app/se-deplacer/prochains-departs/page.tsx @@ -0,0 +1,41 @@ +import React, { Suspense } from 'react'; +import ProchainsDepartsClient from '@/components/ProchainsDepartsClient'; +import { supabase } from '@/lib/supabase'; + +export const metadata = { + title: 'Prochains départs | TER Ferrovia', + description: 'Tableaux des départs et arrivées en gare', +}; + +export default async function ProchainsDepartsPage() { + let menuStructure = null; + try { + const { data: mData } = await supabase + .from('menus_regionaux') + .select('structure') + .eq('actif', true) + .limit(1); + + if (mData && mData.length > 0 && Array.isArray(mData[0].structure)) { + menuStructure = mData[0].structure; + } + } catch (err) { + console.error('Error fetching menuStructure:', err); + } + + return ( + +

+
+

Chargement des prochains départs...

+
+ + }> + + + ); +} diff --git a/src/components/HeaderMenu.tsx b/src/components/HeaderMenu.tsx new file mode 100644 index 0000000..c7f7e6c --- /dev/null +++ b/src/components/HeaderMenu.tsx @@ -0,0 +1,394 @@ +"use client"; + +import React, { useState, useEffect, useRef } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; + +export interface MenuItem { + id?: string; + label: string; + url?: string; + type?: string; + page_id?: string; + sous_elements?: MenuItem[]; + sous_sous_elements?: MenuItem[]; +} + +interface HeaderMenuProps { + menuStructure?: MenuItem[] | null; + regionSlug: string; + activeTab?: string; + onTabSelect?: (label: string) => void; + mobileMenuOpen?: boolean; + onMobileMenuToggle?: (open: boolean) => void; +} + +const DEFAULT_MENU: MenuItem[] = [ + { + id: 'def-1', + label: 'Se déplacer', + url: '/se-deplacer', + sous_elements: [ + { + id: 'def-1-1', + label: 'Rechercher un horaire (train, car...)', + url: '/horaires' + }, + { + id: 'def-1-2', + label: 'Prochains départs', + url: '/se-deplacer/prochains-departs' + }, + { + id: 'def-1-3', + label: 'Gares', + url: '/#gares' + } + ], + sous_sous_elements: [] + }, + { + id: 'def-2', + label: 'Abonnements', + url: '/abonnements', + sous_elements: [ + { + id: 'def-2-1', + label: 'Fiches horaires', + url: '/#fiches-horaires' + }, + { + id: 'def-2-2', + label: 'Info trafic', + url: '/#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-3', + label: 'Tarifs et cartes de réduction', + url: '/tarifs-cartes', + sous_elements: [ + { + id: 'def-3-1', + label: 'Cartes du réseau', + url: '/#cartes-reseau' + }, + { + id: 'def-3-2', + label: 'Mon Info Trafic : alertes perturbations par sms/mail', + url: '/#alertes-sms' + } + ] + }, + { + id: 'def-4', + label: 'Services et contacts', + url: '/services-contacts', + sous_elements: [] + }, + { + id: 'def-5', + label: 'Découvrir la région', + url: '/decouvrir-la-region', + sous_elements: [] + } +]; + +export default function HeaderMenu({ + menuStructure, + regionSlug, + activeTab = "Se déplacer", + onTabSelect, + mobileMenuOpen: controlledMobileMenuOpen, + onMobileMenuToggle, +}: HeaderMenuProps) { + const pathname = usePathname(); + const theme = getRegionTheme(regionSlug); + const [hoveredIndex, setHoveredIndex] = useState(null); + const [internalMobileMenuOpen, setInternalMobileMenuOpen] = useState(false); + const [expandedMobileIndices, setExpandedMobileIndices] = useState>({}); + const timeoutRef = useRef(null); + + const [items, setItems] = useState(menuStructure && menuStructure.length > 0 ? menuStructure : DEFAULT_MENU); + + useEffect(() => { + if (menuStructure && menuStructure.length > 0) { + setItems(menuStructure); + return; + } + + const fetchRegionalMenu = 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 && Array.isArray(mData[0].structure)) { + setItems(mData[0].structure); + } else { + setItems(DEFAULT_MENU); + } + } catch (err) { + console.error('Error fetching menus_regionaux:', err); + } + }; + + fetchRegionalMenu(); + }, [menuStructure, regionSlug]); + + const isMobileMenuOpen = controlledMobileMenuOpen !== undefined ? controlledMobileMenuOpen : internalMobileMenuOpen; + + const setMobileMenuOpen = (open: boolean) => { + if (onMobileMenuToggle) { + onMobileMenuToggle(open); + } else { + setInternalMobileMenuOpen(open); + } + }; + + const handleMouseEnter = (index: number) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setHoveredIndex(index); + }; + + const handleMouseLeave = () => { + timeoutRef.current = setTimeout(() => { + setHoveredIndex(null); + }, 150); + }; + + const resolveUrl = (rawUrl?: string) => { + if (!rawUrl) return `/${regionSlug}`; + if (rawUrl.startsWith('#')) return `/${regionSlug}${rawUrl}`; + if (rawUrl.startsWith('/')) { + if (rawUrl === '/se-deplacer/horaires' || rawUrl === '/horaires') { + return `/${regionSlug}/horaires`; + } + return `/${regionSlug}${rawUrl}`; + } + return rawUrl; + }; + + const toggleMobileIndex = (idx: number) => { + setExpandedMobileIndices(prev => ({ ...prev, [idx]: !prev[idx] })); + }; + + const activeItem = hoveredIndex !== null ? items[hoveredIndex] : null; + + const getSubItems = (item: MenuItem): MenuItem[] => { + const list: MenuItem[] = []; + if (item.sous_elements && Array.isArray(item.sous_elements)) { + list.push(...item.sous_elements); + } + if (item.sous_sous_elements && Array.isArray(item.sous_sous_elements)) { + list.push(...item.sous_sous_elements); + } + return list; + }; + + const activeSubItems = activeItem ? getSubItems(activeItem) : []; + + return ( +
+ {/* DESKTOP NAVIGATION BAR (IMAGE 1 & 2) */} + + + {/* 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) => { + const nestedList = getSubItems(sub); + return ( +
+ setMobileMenuOpen(false)} + className="text-[13px] font-medium text-gray-700 hover:text-[#006837] block py-0.5" + > + {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" + > + {nested.label} + + ))} +
+ )} +
+ ); + })} +
+ )} +
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/components/HorairesPageClient.tsx b/src/components/HorairesPageClient.tsx new file mode 100644 index 0000000..37f49b3 --- /dev/null +++ b/src/components/HorairesPageClient.tsx @@ -0,0 +1,172 @@ +"use client"; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import SiteHeader from '@/components/SiteHeader'; +import TimetableSearchForm from '@/components/TimetableSearchForm'; +import ClientWcsProvider from '@/components/ClientWcsProvider'; + +interface HorairesPageClientProps { + regionSlug: string; + regionData: { + nom_site: string; + logo_header_url: string; + description_site: string; + }; + 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, + regionData, + menuStructure, +}: HorairesPageClientProps) { + const cfg = REGION_CONFIGS[regionSlug] || { + heroImage: DEFAULT_HERO, + logoUrl: regionData.logo_header_url || '', + name: regionSlug.replace(/-/g, ' ').toUpperCase() + }; + + const logoUrl = regionData.logo_header_url || cfg.logoUrl; + const heroImage = cfg.heroImage; + + const [searchFeedback, setSearchFeedback] = useState(null); + + const handleSearchResults = (params: any) => { + setSearchFeedback(`Recherche d'horaires lancée de "${params.departure || 'Gare de départ'}" à "${params.arrival || 'Gare d\'arrivée'}" pour le ${params.outwardDate} à ${params.outwardTime}.`); + }; + + return ( + +
+ + {/* ================================================================== */} + {/* 1. STICKY HEADER COMPONENT WITH REGIONAL LOGO AND MENU */} + {/* ================================================================== */} + + + {/* ================================================================== */} + {/* 3. BREADCRUMB BAR BELOW MENU (IMAGE 1) */} + {/* ================================================================== */} +
+
+ + home + + > + Rechercher un horaire +
+
+ + {/* ================================================================== */} + {/* 4. HERO SECTION WITH SEARCH CARD ON THE LEFT (IMAGE 1) */} + {/* ================================================================== */} +
+
+ {searchFeedback && ( +
+ {searchFeedback} + +
+ )} + + {/* Timetable Search Form card pinned to LEFT side (Image 1) */} +
+ +
+
+
+ + {/* ================================================================== */} + {/* 5. FOOTER */} + {/* ================================================================== */} +
+
+ Accueil {cfg.name} + • + Rechercher un horaire + • + Info Trafic + • + Actualités +
+

© FERROVIA - Tous droits réservés.

+
+ +
+
+ ); +} diff --git a/src/components/OffersPageClient.tsx b/src/components/OffersPageClient.tsx new file mode 100644 index 0000000..782c856 --- /dev/null +++ b/src/components/OffersPageClient.tsx @@ -0,0 +1,1140 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import SiteHeader from '@/components/SiteHeader'; + +interface OffersPageClientProps { + regionSlug?: string; + menuStructure?: any; +} + +export interface TrainStop { + time: string; + stationName: string; +} + +export interface PerturbationInfo { + id?: string; + type?: string; + cause?: string; + causeDetails?: string; + delayMinutes?: number; + notes?: string; +} + +export interface TrainOffer { + id: string; + departureTime: string; + arrivalTime: string; + duration: string; + departureStation: string; + arrivalStation: string; + trainType: string; + trainNumber: string; + finalDestination: string; + operator: string; + trainModel: string; + capacityText: string; + onboardServices: string; + priceClass: string; + price: string; + stops: TrainStop[]; + serviceAnnuelName?: string; + perturbation?: PerturbationInfo | null; + trainModelImageUrl?: string; +} + +const DEFAULT_STOPS_FULL: TrainStop[] = [ + { time: '06:44', stationName: 'Dijon' }, + { time: '06:56', stationName: 'Ouges' }, + { time: '07:00', stationName: 'Saulon' }, + { time: '07:04', stationName: 'Longecourt-en-Plaine' }, + { time: '07:07', stationName: 'Aiserey' }, + { time: '07:12', stationName: 'Brazey-en-Plaine' }, + { time: '07:16', stationName: 'Saint-Jean-de-Losne' }, + { time: '07:19', stationName: 'Chaugey' }, + { time: '07:23', stationName: 'Pagny' }, + { time: '07:28', stationName: 'Seurre' }, + { time: '08:30', stationName: 'Bourg-en-Bresse' }, +]; + +const DEFAULT_OFFERS: TrainOffer[] = [ + { + id: '1', + departureTime: '06:44', + arrivalTime: '07:23', + duration: '39 min', + departureStation: 'Dijon', + arrivalStation: 'Pagny', + trainType: 'TER', + trainNumber: '891801', + finalDestination: 'Bourg-en-Bresse', + operator: 'SNCF Voyageurs - 1187', + trainModel: 'Un train "AGC"', + capacityText: '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: '2nde classe', + price: '6,50 €', + stops: DEFAULT_STOPS_FULL.slice(0, 9), + }, + { + id: '2', + departureTime: '07:30', + arrivalTime: '08:08', + duration: '38 min', + departureStation: 'Dijon', + arrivalStation: 'Pagny', + trainType: 'TER', + trainNumber: '891803', + finalDestination: 'Louhans', + operator: 'SNCF Voyageurs - 1187', + trainModel: 'Un train "AGC"', + capacityText: '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: '2nde classe', + price: '6,50 €', + stops: [ + { time: '07:30', stationName: 'Dijon' }, + { time: '07:42', stationName: 'Ouges' }, + { time: '07:46', stationName: 'Saulon' }, + { time: '07:50', stationName: 'Longecourt-en-Plaine' }, + { time: '07:53', stationName: 'Aiserey' }, + { time: '07:58', stationName: 'Brazey-en-Plaine' }, + { time: '08:02', stationName: 'Saint-Jean-de-Losne' }, + { time: '08:05', stationName: 'Chaugey' }, + { time: '08:08', stationName: 'Pagny' }, + { time: '08:13', stationName: 'Seurre' }, + ], + }, + { + id: '3', + departureTime: '08:44', + arrivalTime: '09:23', + duration: '39 min', + departureStation: 'Dijon', + arrivalStation: 'Pagny', + trainType: 'TER', + trainNumber: '891805', + finalDestination: 'Bourg-en-Bresse', + operator: 'SNCF Voyageurs - 1187', + trainModel: 'Un train "AGC"', + capacityText: '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: '2nde classe', + price: '6,50 €', + stops: DEFAULT_STOPS_FULL.slice(0, 9), + }, + { + id: '4', + departureTime: '12:15', + arrivalTime: '13:48', + duration: '1h33', + departureStation: 'Dijon', + arrivalStation: 'Pagny', + trainType: 'TER', + trainNumber: '418107', + finalDestination: 'Louhans', + operator: 'SNCF Voyageurs - 1187', + trainModel: 'Un train "AGC"', + capacityText: '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: '2nde classe', + price: '6,50 €', + stops: [ + { time: '12:15', stationName: 'Dijon' }, + { time: '13:00', stationName: 'Saint-Jean-de-Losne' }, + { time: '13:48', stationName: 'Pagny' }, + ], + }, + { + id: '5', + departureTime: '16:44', + arrivalTime: '17:23', + duration: '39 min', + departureStation: 'Dijon', + arrivalStation: 'Pagny', + trainType: 'TER', + trainNumber: '891811', + finalDestination: 'Bourg-en-Bresse', + operator: 'SNCF Voyageurs - 1187', + trainModel: 'Un train "AGC"', + capacityText: '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: 'À partir de', + price: '6,00 €', + stops: DEFAULT_STOPS_FULL.slice(0, 9), + }, +]; + +export default function OffersPageClient({ + regionSlug = 'bourgogne-franche-comte', + menuStructure, +}: OffersPageClientProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Search parameters from URL + const departure = searchParams.get('departure') || 'Dijon'; + const arrival = searchParams.get('arrival') || 'Pagny'; + const outwardDate = searchParams.get('outwardDate') || 'Lundi 21 septembre'; + const outwardTime = searchParams.get('outwardTime') || '00:00'; + const passengerCount = searchParams.get('passengers') || '1'; + const passengerAge = searchParams.get('passengerAge') || '30'; + const discountCard = searchParams.get('discountCard') || 'sans carte'; + + const [offers, setOffers] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedDetailsId, setExpandedDetailsId] = useState(null); + const [selectedModalOffer, setSelectedModalOffer] = useState(null); + const [currentServiceAnnuel, setCurrentServiceAnnuel] = useState(null); + + // Robust station matching helper (exact or prefix match to prevent false partial matches) + const matchStation = (query: string, stationNom: string): boolean => { + if (!query || !stationNom) return false; + const clean = (s: string) => (s || '') + .toLowerCase() + .normalize("NFD").replace(/[\u0300-\u036f]/g, "") + .replace(/\s*\(toutes gares\)/g, '') + .replace(/gare de /g, '') + .replace(/gare d'/g, '') + .trim(); + const q = clean(query); + const st = clean(stationNom); + if (!q || !st) return false; + return q === st || st.startsWith(q) || q.startsWith(st); + }; + + // Helper to extract exact segment stops & times for user departure -> arrival segment + const resolveTrainSegment = (item: any, depQuery: string, arrQuery: string, gMap: Record) => { + let stops: TrainStop[] = []; + + if (item.gares_desservies && Array.isArray(item.gares_desservies) && item.gares_desservies.length > 0) { + stops = item.gares_desservies.map((g: any) => ({ + time: (g.heure_depart || g.heure_arrivee || '00:00').slice(0, 5), + stationName: g.gare_nom || (g.gare_id ? gMap[g.gare_id] : ''), + })); + } else { + stops = [ + { time: (item.heure_depart || '08:00').slice(0, 5), stationName: item.gare_depart_nom || (item.gare_depart_id ? gMap[item.gare_depart_id] : '') || depQuery }, + { time: (item.heure_arrivee || '08:45').slice(0, 5), stationName: item.gare_arrivee_nom || (item.gare_arrivee_id ? gMap[item.gare_arrivee_id] : '') || arrQuery }, + ]; + } + + const depIdx = stops.findIndex(s => matchStation(depQuery, s.stationName)); + const arrIdx = stops.findIndex(s => matchStation(arrQuery, s.stationName)); + + // STRICT DIRECTION FILTERING: Train MUST travel in direction depIdx < arrIdx! + if (depIdx === -1 || arrIdx === -1 || depIdx >= arrIdx) { + return null; + } + + const segStops = stops.slice(depIdx, arrIdx + 1); + const segDepTime = stops[depIdx].time; + const segArrTime = stops[arrIdx].time; + const segDepName = stops[depIdx].stationName; + const segArrName = stops[arrIdx].stationName; + + // Calculate segment duration + const [h1, m1] = segDepTime.split(':').map(Number); + const [h2, m2] = segArrTime.split(':').map(Number); + let totalMins = (h2 * 60 + m2) - (h1 * 60 + m1); + if (totalMins < 0) totalMins += 1440; + const durHours = Math.floor(totalMins / 60); + const durMins = totalMins % 60; + const durStr = durHours > 0 ? `${durHours}h${durMins < 10 ? '0' : ''}${durMins}` : `${durMins} min`; + + const finalDest = stops[stops.length - 1]?.stationName || item.gare_arrivee_nom || arrQuery; + + return { + depTime: segDepTime, + arrTime: segArrTime, + duration: durStr, + depName: segDepName, + arrName: segArrName, + finalDestination: finalDest, + segmentStops: segStops, + }; + }; + + // Helper function to convert French text dates or slash dates to YYYY-MM-DD + const formatToIsoDate = (dateStr: string, defaultYear = 2026): string => { + if (!dateStr) return `${defaultYear}-09-19`; + const trimmed = dateStr.trim(); + + if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { + return trimmed; + } + + const slashMatch = trimmed.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/); + if (slashMatch) { + return `${slashMatch[3]}-${slashMatch[2].padStart(2, '0')}-${slashMatch[1].padStart(2, '0')}`; + } + + const monthMap: Record = { + janv: '01', janvier: '01', + fevr: '02', févr: '02', fevrier: '02', février: '02', + mars: '03', + avr: '04', avril: '04', + mai: '05', + juin: '06', + juil: '07', juillet: '07', + aout: '08', août: '08', + sept: '09', septembre: '09', + oct: '10', octobre: '10', + nov: '11', novembre: '11', + dec: '12', déc: '12', decembre: '12', décembre: '12', + }; + + const lower = trimmed.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + const dayMatch = lower.match(/\b(\d{1,2})\b/); + const dayNum = dayMatch ? dayMatch[1].padStart(2, '0') : '19'; + const yearMatch = lower.match(/\b(20\d{2})\b/); + const year = yearMatch ? yearMatch[1] : defaultYear.toString(); + + let monthStr = '09'; + for (const [key, val] of Object.entries(monthMap)) { + const keyClean = key.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + if (lower.includes(keyClean)) { + monthStr = val; + break; + } + } + + return `${year}-${monthStr}-${dayNum}`; + }; + + // Fetch train offers from Supabase BDD with strict segment filtering & sorting by departure time + useEffect(() => { + const fetchTrainOffers = async () => { + setLoading(true); + try { + // 1. Fetch gares mapping + const { data: garesData } = await supabase.from('gares').select('id, nom'); + const gMap: Record = {}; + if (garesData) { + garesData.forEach((g: any) => { gMap[g.id] = g.nom; }); + } + + // Target ISO date string (YYYY-MM-DD) from user selected date + const targetIsoDate = formatToIsoDate(outwardDate); + + // 2. Fetch services_annuels to determine active SA for targetIsoDate + const { data: saData } = await supabase + .from('services_annuels') + .select('*') + .eq('actif', true); + + const matchingSA = saData?.find((sa: any) => { + const dStart = (sa.date_debut || '').slice(0, 10); + const dEnd = (sa.date_fin || '').slice(0, 10); + return dStart <= targetIsoDate && targetIsoDate <= dEnd; + }); + + if (matchingSA) { + setCurrentServiceAnnuel(`${matchingSA.nom} (${matchingSA.code || 'SA'})`); + } else { + setCurrentServiceAnnuel(null); + } + + // 3. Fetch perturbations from Supabase + const { data: pertData } = await supabase + .from('perturbations') + .select('*') + .eq('actif', true) + .is('deleted_at', null); + + // 4. Query horaires from Supabase + const { data: hData } = await supabase + .from('horaires') + .select('*') + .eq('actif', true) + .is('deleted_at', null); + + // Determine day of week column from outwardDate + const dayLower = (outwardDate || '').toLowerCase(); + let targetCol = 'circule_lundi'; + if (dayLower.includes('sam')) targetCol = 'circule_samedi'; + else if (dayLower.includes('dim')) targetCol = 'circule_dimanche'; + else if (dayLower.includes('lun')) targetCol = 'circule_lundi'; + else if (dayLower.includes('mar')) targetCol = 'circule_mardi'; + else if (dayLower.includes('mer')) targetCol = 'circule_mercredi'; + else if (dayLower.includes('jeu')) targetCol = 'circule_jeudi'; + else if (dayLower.includes('ven')) targetCol = 'circule_vendredi'; + + // Check circulation against jours_personnalises first, then fallback to day column + const filterDay = (item: any): boolean => { + if (item.jours_personnalises && Array.isArray(item.jours_personnalises) && item.jours_personnalises.length > 0) { + const dateEntry = item.jours_personnalises.find((jp: any) => { + const d = (jp.date || jp.jour || '').slice(0, 10); + return d === targetIsoDate; + }); + return dateEntry?.circule === true; + } + + if (item[targetCol] !== undefined && item[targetCol] !== null) { + return Boolean(item[targetCol]); + } + return true; + }; + + const userTimeHHMM = (outwardTime && outwardTime !== '--:--') ? outwardTime.slice(0, 5) : '00:00'; + + if (hData && hData.length > 0) { + const mappedList: TrainOffer[] = []; + const seenKeys = new Set(); + + hData.forEach((item: any, idx: number) => { + // Service Annuel Filter: Match service_annuel_id if a SA covers targetIsoDate + if (matchingSA && item.service_annuel_id && item.service_annuel_id !== matchingSA.id) { + return; + } + + if (!filterDay(item)) return; + + const seg = resolveTrainSegment(item, departure, arrival, gMap); + if (!seg) return; + + // Check if segment departure time is >= user's chosen time + if (seg.depTime < userTimeHHMM) return; + + // Deduplicate same train departure time & number + const uniqueKey = `${seg.depTime}_${seg.depName}_${seg.arrName}_${item.numero_train || idx}`; + if (seenKeys.has(uniqueKey)) return; + seenKeys.add(uniqueKey); + + // Match active perturbation for this train + const matchingPert = pertData?.find((p: any) => { + const matchTrain = p.horaire_id === item.id || p.horaire_id === item.numero_train; + if (!matchTrain) return false; + + const pStart = (p.date_debut || '').slice(0, 10); + const pEnd = (p.date_fin || '').slice(0, 10); + + if (pStart && pEnd && pStart <= targetIsoDate && targetIsoDate <= pEnd) { + return true; + } + if (p.jours_perturbation && Array.isArray(p.jours_perturbation)) { + const jpMatch = p.jours_perturbation.some((jp: any) => { + const d = (jp.date || jp.dateDebut || '').slice(0, 10); + return d === targetIsoDate; + }); + if (jpMatch) return true; + } + return !pStart && !pEnd && (!p.jours_perturbation || p.jours_perturbation.length === 0); + }); + + const pertInfo: PerturbationInfo | null = matchingPert ? { + id: matchingPert.id?.toString(), + type: matchingPert.type_perturbation || 'Perturbation', + cause: matchingPert.cause || 'Incident de circulation', + causeDetails: matchingPert.cause_details || matchingPert.notes || '', + delayMinutes: matchingPert.duree_retard_minutes || 0, + notes: matchingPert.notes || '', + } : null; + + // Determine price based on segment duration or distance + const isShortRoute = seg.arrName.toLowerCase().includes('pagny') || seg.arrName.toLowerCase().includes('brazey') || seg.depName.toLowerCase().includes('brazey'); + const priceVal = isShortRoute ? '6,50 €' : '13,00 €'; + + // Extract matériel roulant image from composition_train BDD + const composition = Array.isArray(item.composition_train) ? item.composition_train : []; + const firstMat = composition.length > 0 ? composition[0] : null; + const matImageUrl = firstMat?.materiel_image_url || 'https://supabase-ferrovia.mrpatator.fr/storage/v1/object/public/materiel-roulant/images/1789909846752_regiolis_firsttosecond_bluegreenmobigo_4c__1_.png'; + const matNameText = firstMat ? `Un train "${firstMat.materiel_nom || 'Régiolis'}" ${firstMat.materiel_nom_technique ? `(${firstMat.materiel_nom_technique})` : ''}` : (item.modele_train || 'Un train "AGC"'); + + mappedList.push({ + id: item.id?.toString() || idx.toString(), + departureTime: seg.depTime, + arrivalTime: seg.arrTime, + duration: seg.duration, + departureStation: seg.depName, + arrivalStation: seg.arrName, + trainType: item.type_train || 'TER', + trainNumber: item.numero_train || `891${idx + 801}`, + finalDestination: seg.finalDestination, + operator: item.operateur || 'SNCF Voyageurs - 1187', + trainModel: matNameText, + capacityText: item.capacite_texte || '4 voitures - 220 places', + onboardServices: '🚲', + priceClass: idx === hData.length - 1 ? 'À partir de' : '2nd classe', + price: priceVal, + stops: seg.segmentStops, + serviceAnnuelName: matchingSA ? `${matchingSA.nom} (${matchingSA.code || ''})` : undefined, + perturbation: pertInfo, + trainModelImageUrl: matImageUrl, + }); + }); + + if (mappedList.length > 0) { + // SORT STRICTLY BY DEPARTURE TIME ASCENDING + mappedList.sort((a, b) => a.departureTime.localeCompare(b.departureTime)); + setOffers(mappedList); + setLoading(false); + return; + } + } + + // Fallback matching default sample offers if DB yields no rows for custom gares + const filteredDefault = DEFAULT_OFFERS.map(o => { + const isDijonSeurre = arrival.toLowerCase().includes('seurre'); + let arrSt = arrival; + let arrTm = o.arrivalTime; + let durSt = o.duration; + let priceSt = o.price; + let stopsList = o.stops; + + if (isDijonSeurre) { + arrSt = 'Seurre'; + arrTm = o.departureTime === '06:44' ? '07:28' : o.departureTime === '07:30' ? '08:13' : '09:28'; + durSt = '44 min'; + priceSt = '13,00 €'; + stopsList = DEFAULT_STOPS_FULL; + } else { + stopsList = o.stops.map((s, idx) => idx === o.stops.length - 1 ? { ...s, stationName: arrival } : s); + } + + return { + ...o, + departureStation: departure, + arrivalStation: arrSt, + arrivalTime: arrTm, + duration: durSt, + price: priceSt, + stops: stopsList, + }; + }).filter(o => o.departureTime >= userTimeHHMM); + + filteredDefault.sort((a, b) => a.departureTime.localeCompare(b.departureTime)); + setOffers(filteredDefault); + + } catch (err) { + console.error('Error fetching horaires from Supabase:', err); + const filtered = DEFAULT_OFFERS.filter(o => o.departureTime >= (outwardTime.slice(0, 5) || '00:00')); + filtered.sort((a, b) => a.departureTime.localeCompare(b.departureTime)); + setOffers(filtered); + } finally { + setLoading(false); + } + }; + + fetchTrainOffers(); + }, [departure, arrival, outwardDate, outwardTime]); + + const handleModifySearch = () => { + const searchUrl = regionSlug ? `/${regionSlug}/horaires` : '/horaires'; + router.push(searchUrl); + }; + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+
+ {/* ─── STICKY HEADER COMPONENT ─── */} + + + {/* ─── MAIN CONTENT AREA (STRICTLY CONFORME IMG 1 & 2) ─── */} +
+ + {/* Mobile Summary Banner (Img 2) */} +
+
+ +
+
+ {departure} → {arrival} +
+
+ {outwardDate.toLowerCase()} , {passengerCount} voyageur{Number(passengerCount) > 1 ? 's' : ''} +
+
+
+
+ + {/* Page Title & Subtitle Link */} +
+

+ Sélectionner votre billet +

+ +
+ + {/* Grid Layout: Left Results Column + Right Recap Sidebar */} +
+ + {/* ─── LEFT COLUMN: TRAIN OFFERS LIST (IMG 1 & 2) ─── */} +
+ + {/* Filter / Date Row Header */} +
+
+ event + + {outwardDate.includes('sept') || outwardDate.includes('oct') || outwardDate.includes('nov') ? outwardDate : `Date : ${outwardDate}`} + + {currentServiceAnnuel && ( + + calendar_today + Service Annuel : {currentServiceAnnuel} + + )} +
+ +
+ + {/* Train Result Cards */} +
+ {loading ? ( +
+ refresh +

Recherche des horaires en cours dans la base Supabase...

+
+ ) : offers.length > 0 ? ( + offers.map((offer) => { + const isExpanded = expandedDetailsId === offer.id; + return ( +
+ {/* PERTURBATION WARNING BANNER */} + {offer.perturbation && ( +
+
+ warning + Perturbation : + {offer.perturbation.cause || offer.perturbation.type || 'Incident de circulation'} +
+ {offer.perturbation.delayMinutes ? ( + + Retard : +{offer.perturbation.delayMinutes} min + + ) : null} +
+ )} + + {/* MAIN CARD ROW (CONFORME IMG 1 DESKTOP & IMG 2 MOBILE) */} +
+ + {/* LEFT/CENTER CONTENT BLOCK */} +
+ + {/* TIMELINE ROW */} +
+ + {/* Departure Block */} +
+ + {offer.departureTime} + + + {offer.departureStation} + +
+ + {/* Horizontal duration timeline matching Img 1 & 2 */} +
+ +
+ + {offer.duration} + +
+ +
+ + {/* Arrival Block */} +
+ + {offer.arrivalTime} + + + {offer.arrivalStation} + +
+ +
+ + {/* METADATA ROW: Train Icon/Number + Details Button (Img 1 desktop & Img 2 mobile) */} +
+ + {/* Details Pill Button (Bottom left on desktop & mobile) */} +
+ +
+ + {/* Train Icon & Number (Under Pagny on Desktop, Under Dijon on Mobile) */} +
+ directions_railway + {offer.trainType} {offer.trainNumber} +
+ +
+ +
+ + {/* PRICE BUTTON BADGE (CONFORME IMG 1 & 2) */} +
+ +
+ +
+ + {/* ─── EXPANDED ACCORDION DETAILS (STRICTLY CONFORME IMG 1) ─── */} + {isExpanded && ( +
+ + {/* LEFT COLUMN: TIMELINE & TRAIN DETAILS (IMG 1) */} +
+ + {/* Departure node */} +
+ + {offer.departureTime} + + directions_railway + {offer.departureStation} +
+ + {/* Vertical line & details block */} +
+ {/* Left vertical line with duration */} +
+
+ + {offer.duration} + +
+
+ + {/* Train info matching Image 1 */} +
+ + {/* Clickable Underlined Train Link -> Opens Modal (Image 2) */} + + +

+ Destination: {offer.finalDestination} +

+

+ Opéré par {offer.operator} +

+ + {/* Perturbation Accordion Alert */} + {offer.perturbation && ( +
+
+ warning + Information Perturbation +
+

Cause : {offer.perturbation.cause}

+ {offer.perturbation.causeDetails &&

{offer.perturbation.causeDetails}

} + {offer.perturbation.delayMinutes ?

Retard estimé : +{offer.perturbation.delayMinutes} min

: null} +
+ )} + + {/* Matériel Roulant Image asset + Green Right Arrow Button */} +
+
+ {offer.trainModel +
+ +
+ +

+ {offer.trainModel} +

+

+ {offer.capacityText} +

+ + {/* Bicycle icon */} +
+ À bord : + pedal_bike +
+
+
+ + {/* Arrival node */} +
+ + {offer.arrivalTime} + + place + {offer.arrivalStation} +
+ +
+ + {/* RIGHT COLUMN: FARE / PASSENGER CARD INSIDE ACCORDION (IMAGE 1) */} +
+
+ +
+
+
+ + person + +
+

+ Tarif jeune TRAIN Mobigo -26 +

+

+ Voyageur 1 +

+
+
+ + {offer.price} + +
+ +
+

Adulte - Remboursable

+ +
+
+ + {/* Green price button at bottom right of card */} +
+ +
+ +
+
+ +
+ )} + +
+ ); + }) + ) : ( +
+ train +

+ Aucun horaire trouvé pour ce trajet +

+

+ Aucun train ne circule entre {departure} et {arrival} le {outwardDate} après {outwardTime} dans la base de données. +

+ +
+ )} +
+ + {/* Horaires suivants link */} +
+ +
+ + {/* Haut de page Pill Button (CONFORME IMG 1 & 2) */} +
+ +
+ +
+ + {/* ─── RIGHT SIDEBAR: SEARCH SUMMARY CARD (STRICTLY CONFORME IMG 1 & 2) ─── */} +
+
+ +
+ Aller le {outwardDate.toLowerCase().includes('lun') ? outwardDate : 'lun. 21 sept.'} à {outwardTime} +
+ +
+ {departure} → {arrival} +
+ +
+ {passengerCount} voyageur{parseInt(passengerCount) > 1 ? 's' : ''}, {passengerAge} ans, {discountCard} +
+ + {/* Modifier ma recherche button */} +
+ +
+ +
+
+ +
+ +
+
+ + {/* ─── MODALE ETAPES DU TRAIN (STRICTLY CONFORME IMG 2) ─── */} + {selectedModalOffer && ( +
setSelectedModalOffer(null)} + > +
e.stopPropagation()} + > + {/* Top right close button */} + + + {/* Header Title: Gare Départ -> Gare Arrivée */} +

+ {selectedModalOffer.departureStation} → {selectedModalOffer.arrivalStation} +

+ + {/* Table Headers */} +
+ Heure de passage + Etape +
+ + {/* Timeline List of Stops (IMAGE 2) */} +
+ {selectedModalOffer.stops && selectedModalOffer.stops.length > 0 ? ( +
+ {/* Green vertical line connecting all dots */} +
+ + {selectedModalOffer.stops.map((stop, idx) => ( +
+ {/* Time */} + + {stop.time} + + + {/* Green Dot */} +
+ +
+ + {/* Station Name */} + + {stop.stationName} + +
+ ))} +
+ ) : ( +
+ Information d'étapes non disponible. +
+ )} +
+ +
+
+ )} + + {/* ─── FOOTER SECTION ─── */} +
+
+ + {/* Region Selector Bar & Logos Row */} +
+ + {/* Region Dropdown Selector */} +
+ +
+ + {/* Regional & Network Logos */} +
+ Logos SNCF et Mobigo +
+ +
+ +
+ + {/* Social Icons Row */} + + + {/* Legal Links Row */} + + + + +
+
+ +
+ ); +} diff --git a/src/components/ProchainsDepartsClient.tsx b/src/components/ProchainsDepartsClient.tsx new file mode 100644 index 0000000..01da5a7 --- /dev/null +++ b/src/components/ProchainsDepartsClient.tsx @@ -0,0 +1,1865 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useRouter, useSearchParams, usePathname } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; +import SiteHeader from '@/components/SiteHeader'; +import { getRegionTheme } from '@/lib/regionTheme'; +import { isPerturbationActiveOnDate, getPerturbationBannerStyle } from '@/lib/perturbationUtils'; + +interface ProchainsDepartsClientProps { + regionSlug?: string; + menuStructure?: any; +} + +export interface StationOption { + id: string; + nom: string; + type?: string; +} + +export interface DepartureRow { + id: string; + departTime: string; + revisedTime?: string; + delayText?: string; + isSupprime?: boolean; + destination: string; + modeIcon: 'bus' | 'train'; + modeLabel: string; + trainNumber: string; + trainBadge?: string; + operator?: string; + voie: string; + rawQuai?: string; + dateIso?: string; // e.g. "2026-09-20" or "2026-09-21" + dateLabel?: string; // e.g. "Lundi 21 septembre" + perturbationType?: 'retard' | 'suppression' | 'information' | 'parcours_modifie' | string; + perturbationDetails?: string; + infoBadge?: string; + travauxBanner?: string; + stops: { + departTime: string; + gareNom: string; + voie: string; + isDepart?: boolean; + isArrivee?: boolean; + }[]; +} + +export interface ArrivalRow { + id: string; + arrivalTime: string; + revisedTime?: string; + delayText?: string; + isSupprime?: boolean; + provenance: string; + modeIcon: 'bus' | 'train'; + modeLabel: string; + trainNumber: string; + trainBadge?: string; + operator?: string; + voie: string; + dateIso?: string; + dateLabel?: string; + perturbationType?: 'retard' | 'suppression' | 'information' | 'parcours_modifie' | string; + perturbationDetails?: string; + stops: { + departTime: string; + gareNom: string; + voie: string; + isDepart?: boolean; + isArrivee?: boolean; + }[]; +} + +export interface TravauxItem { + id: string; + titre: string; + description: string; + ligne_nom?: string; +} + +// ─── BRAZEY-EN-PLAINE DEFAULT DATASET (STRICTLY CONFORME IMAGE 1) ─── +const DEFAULT_BRAZEY_DEPARTURES: DepartureRow[] = [ + { + id: 'b1', + departTime: '18:50', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891816', + trainBadge: 'P7', + voie: '2', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '18:50', gareNom: 'Brazey-en-Plaine', voie: '2', isDepart: true }, + { departTime: '18:55', gareNom: 'Aiserey', voie: '2' }, + { departTime: '18:59', gareNom: 'Longecourt-en-Plaine', voie: '-' }, + { departTime: '19:03', gareNom: 'Saulon', voie: '-' }, + { departTime: '19:07', gareNom: 'Ouges', voie: '-' }, + { departTime: '19:16', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b2', + departTime: '19:12', + destination: 'Bourg-en-Bresse', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891817', + voie: '1', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '19:12', gareNom: 'Brazey-en-Plaine', voie: '1', isDepart: true }, + { departTime: '19:45', gareNom: 'Seurre', voie: '1' }, + { departTime: '20:30', gareNom: 'Bourg-en-Bresse', voie: '2', isArrivee: true }, + ] + }, + { + id: 'b3', + departTime: '19:49', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891818', + voie: '2', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '19:49', gareNom: 'Brazey-en-Plaine', voie: '2', isDepart: true }, + { departTime: '20:15', gareNom: 'Dijon', voie: '1', isArrivee: true }, + ] + }, + { + id: 'b4', + departTime: '20:33', + destination: 'Seurre', + modeIcon: 'bus', + modeLabel: 'Car TER', + trainNumber: '37253', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '20:33', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '21:05', gareNom: 'Seurre', voie: '-', isArrivee: true }, + ] + }, + // Date Transition to Next Day + { + id: 'b5', + departTime: '06:49', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891800', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + dateLabel: 'Lundi 21 septembre', + stops: [ + { departTime: '06:49', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '07:15', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b6', + departTime: '07:12', + destination: 'Bourg-en-Bresse', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891801', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '07:12', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '08:30', gareNom: 'Bourg-en-Bresse', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b7', + departTime: '07:17', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891802', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '07:17', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '07:45', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b8', + departTime: '07:49', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891804', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '07:49', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '08:15', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b9', + departTime: '07:58', + destination: 'Seurre', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891803', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '07:58', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '08:25', gareNom: 'Seurre', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b10', + departTime: '08:50', + destination: 'Dijon', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891806', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '08:50', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '09:16', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b11', + departTime: '09:12', + destination: 'Seurre', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '891805', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '09:12', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '09:40', gareNom: 'Seurre', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b12', + departTime: '13:09', + destination: 'Seurre', + modeIcon: 'bus', + modeLabel: 'Car TER', + trainNumber: '418107', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '13:09', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '13:40', gareNom: 'Seurre', voie: '-', isArrivee: true }, + ] + }, + { + id: 'b13', + departTime: '14:39', + destination: 'Dijon', + modeIcon: 'bus', + modeLabel: 'Car TER', + trainNumber: '418098', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-21', + stops: [ + { departTime: '14:39', gareNom: 'Brazey-en-Plaine', voie: '-', isDepart: true }, + { departTime: '15:10', gareNom: 'Dijon', voie: '-', isArrivee: true }, + ] + } +]; + +// ─── LILLE FLANDRES DEFAULT DATASET (STRICTLY CONFORME IMAGE 2) ─── +const DEFAULT_LILLE_DEPARTURES: DepartureRow[] = [ + { + id: 'lf1', + departTime: '16:29', + destination: 'Kortrijk', + modeIcon: 'bus', + modeLabel: 'Car TER', + trainNumber: '85632', + voie: '-', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:29', gareNom: 'Lille Flandres', voie: '-', isDepart: true }, + { departTime: '17:10', gareNom: 'Kortrijk', voie: '-', isArrivee: true } + ] + }, + { + id: 'lf2', + departTime: '16:35', + destination: 'Valenciennes', + modeIcon: 'train', + modeLabel: 'Train TER HDF', + trainNumber: '', + trainBadge: 'K60', + voie: '10', + perturbationType: 'information', + perturbationDetails: 'Informations', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:35', gareNom: 'Lille Flandres', voie: '10', isDepart: true }, + { departTime: '17:15', gareNom: 'Valenciennes', voie: '2', isArrivee: true } + ] + }, + { + id: 'lf3', + departTime: '16:38', + destination: 'Calais Ville', + modeIcon: 'train', + modeLabel: 'Train TER HDF', + trainNumber: '', + trainBadge: 'K71', + voie: '14', + perturbationType: 'information', + perturbationDetails: 'Informations', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:38', gareNom: 'Lille Flandres', voie: '14', isDepart: true }, + { departTime: '17:40', gareNom: 'Calais Ville', voie: '1', isArrivee: true } + ] + }, + { + id: 'lf4', + departTime: '16:42', + destination: 'Saint-Pol-sur-Ternoise', + modeIcon: 'train', + modeLabel: 'Train TER HDF', + trainNumber: '', + trainBadge: 'K50', + voie: '2', + perturbationType: 'information', + perturbationDetails: 'Information', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:42', gareNom: 'Lille Flandres', voie: '2', isDepart: true }, + { departTime: '17:50', gareNom: 'Saint-Pol-sur-Ternoise', voie: '1', isArrivee: true } + ] + }, + { + id: 'lf5', + departTime: '16:47', + destination: 'Lens', + modeIcon: 'train', + modeLabel: 'Train TER HDF', + trainNumber: '', + trainBadge: 'C41', + voie: '4', + perturbationType: 'information', + perturbationDetails: 'Informations', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:47', gareNom: 'Lille Flandres', voie: '4', isDepart: true }, + { departTime: '17:25', gareNom: 'Lens', voie: '2', isArrivee: true } + ] + }, + { + id: 'lf6', + departTime: '16:50', + destination: 'Paris Nord', + modeIcon: 'train', + modeLabel: 'Train TGV INOUI', + trainNumber: '7060', + voie: '-', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:50', gareNom: 'Lille Flandres', voie: '-', isDepart: true }, + { departTime: '17:52', gareNom: 'Paris Nord', voie: ' Hall 1', isArrivee: true } + ] + }, + { + id: 'lf7', + departTime: '16:54', + destination: 'Tournai', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '19947', + voie: '11', + perturbationType: 'information', + perturbationDetails: 'Information', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:54', gareNom: 'Lille Flandres', voie: '11', isDepart: true }, + { departTime: '17:25', gareNom: 'Tournai', voie: '1', isArrivee: true } + ] + }, + { + id: 'lf8', + departTime: '17:05', + destination: 'Rouen Rive Droite', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '848910', + voie: '-', + perturbationType: 'information', + perturbationDetails: 'Information', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '17:05', gareNom: 'Lille Flandres', voie: '-', isDepart: true }, + { departTime: '19:40', gareNom: 'Rouen Rive Droite', voie: '1', isArrivee: true } + ] + }, + { + id: 'lf9', + departTime: '17:05', + destination: 'Valenciennes', + modeIcon: 'train', + modeLabel: 'Train TER HDF', + trainNumber: '', + trainBadge: 'K60', + voie: '-', + perturbationType: 'information', + perturbationDetails: 'Information', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '17:05', gareNom: 'Lille Flandres', voie: '-', isDepart: true }, + { departTime: '17:45', gareNom: 'Valenciennes', voie: '3', isArrivee: true } + ] + } +]; + +// ─── DIJON DEFAULT DATASET ─── +const DEFAULT_DIJON_DEPARTURES: DepartureRow[] = [ + { + id: 'd1', + departTime: '15:30', + destination: 'Auxerre Saint-Gervais', + modeIcon: 'bus', + modeLabel: 'Car TER', + trainNumber: '79006', + voie: '-', + perturbationType: 'information', + perturbationDetails: 'Car supplémentaire', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '15:30', gareNom: 'Dijon', voie: '-' }, + { departTime: '16:15', gareNom: 'Sombernon', voie: '-' }, + { departTime: '17:00', gareNom: 'Avallon', voie: '-' }, + { departTime: '17:45', gareNom: 'Auxerre Saint-Gervais', voie: '-' }, + ] + }, + { + id: 'd4', + departTime: '15:47', + destination: 'Nevers', + modeIcon: 'train', + modeLabel: 'Train MOBIGO', + trainNumber: '893019', + trainBadge: 'K6', + voie: '-', + operator: 'SNCF Voyageurs - 1187', + dateIso: '2026-09-20', + stops: [ + { departTime: '15:47', gareNom: 'Dijon', voie: '-', isDepart: true }, + { departTime: '16:08', gareNom: 'Beaune', voie: 'A' }, + { departTime: '16:35', gareNom: 'Montchanin', voie: '2' }, + { departTime: '16:42', gareNom: 'Le Creusot', voie: '2' }, + { departTime: '16:58', gareNom: 'Étang', voie: '2' }, + { departTime: '17:12', gareNom: 'Luzy', voie: '2' }, + { departTime: '17:30', gareNom: 'Cercy-la-Tour', voie: '2' }, + { departTime: '17:40', gareNom: 'Decize', voie: 'A' }, + { departTime: '17:55', gareNom: 'Imphy', voie: '2' }, + { departTime: '18:06', gareNom: 'Nevers', voie: '-', isArrivee: true }, + ] + }, + { + id: 'd10', + departTime: '16:49', + destination: 'Lyon Part Dieu', + modeIcon: 'train', + modeLabel: 'Train TER', + trainNumber: '17775', + voie: '-', + perturbationType: 'information', + perturbationDetails: 'Information voyageurs', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '16:49', gareNom: 'Dijon', voie: '-' }, + { departTime: '18:30', gareNom: 'Lyon Part Dieu', voie: 'A' }, + ] + }, + { + id: 'd17', + departTime: '17:50', + revisedTime: '18:00', + delayText: 'Retard estimé de 10 min (Travaux sur la voie)', + destination: 'Metz', + modeIcon: 'train', + modeLabel: 'Train TGV INOUI', + trainNumber: '5500', + voie: '-', + perturbationType: 'retard', + perturbationDetails: 'Retard estimé de 10 min : Travaux sur la voie', + operator: 'SNCF Voyageurs', + dateIso: '2026-09-20', + stops: [ + { departTime: '18:00', gareNom: 'Dijon', voie: '-' }, + { departTime: '20:30', gareNom: 'Metz Ville', voie: '1' }, + ] + } +]; + +export default function ProchainsDepartsClient({ + regionSlug = 'bourgogne-franche-comte', + menuStructure +}: ProchainsDepartsClientProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + + // Dynamic regional theme + const theme = getRegionTheme(regionSlug); + + // URL Query Param state for selected station + const stationParam = searchParams.get('gare') || searchParams.get('station') || ''; + + // Form search state + const [stationQuery, setStationQuery] = useState(stationParam); + const [selectedStation, setSelectedStation] = useState(stationParam); + const [activeTabType, setActiveTabType] = useState<'departs' | 'arrivees'>('departs'); + const [activeSubTab, setActiveSubTab] = useState<'departs' | 'arrivees'>('departs'); + + // Autocomplete stations list + const [stationsList, setStationsList] = useState([]); + const [filteredOptions, setFilteredOptions] = useState([]); + const [showDropdown, setShowDropdown] = useState(false); + + // Departures list state & Travaux state + const [departures, setDepartures] = useState([]); + const [arrivals, setArrivals] = useState([]); + const [travauxList, setTravauxList] = useState([]); + const [loading, setLoading] = useState(false); + + // Accordion open detail ID + const [expandedId, setExpandedId] = useState(null); + + // Get current time as HH:MM string + const getCurrentTimeStr = (): string => { + const now = new Date(); + return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; + }; + + // Get today's date as YYYY-MM-DD + const getTodayIso = (): string => { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + }; + + // Filter out past departures (if dateIso is today and departTime < current time) + const filterPastItems = (items: T[], timeKey: keyof T): T[] => { + const todayIso = getTodayIso(); + const nowStr = getCurrentTimeStr(); + return items.filter(item => { + // If dateIso is today, check if time has passed + if (item.dateIso === todayIso) { + const timeVal = item[timeKey] as unknown as string; + if (timeVal && timeVal < nowStr) return false; + } + // Future dates or no dateIso: always show + return true; + }); + }; + + // Generate arrivals from full horaires data (trains arriving AT this station) + const generateArrivals = (allDepartureRows: DepartureRow[], stationName: string): ArrivalRow[] => { + const cleanName = stationName.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim(); + const arrList: ArrivalRow[] = []; + + allDepartureRows.forEach((dep) => { + const stops = dep.stops; + if (!stops || stops.length < 2) return; + + // Find this station in stops + const stationIdx = stops.findIndex(s => { + const sClean = s.gareNom.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim(); + return sClean.includes(cleanName) || cleanName.includes(sClean); + }); + + // For arrivals, the station must NOT be the first stop (it must arrive there from somewhere) + // stationIdx > 0: train arrives at this station from somewhere else + if (stationIdx > 0) { + const stationStop = stops[stationIdx]; + const firstStop = stops[0]; + // Use arriveeTime if available, otherwise use departTime of that stop + const arrTime = stationStop.arriveeTime || dep.departTime; + + arrList.push({ + id: `arr-${dep.id}`, + arrivalTime: arrTime, + revisedTime: dep.revisedTime, + delayText: dep.delayText, + isSupprime: dep.isSupprime, + provenance: firstStop.gareNom, + modeIcon: dep.modeIcon, + modeLabel: dep.modeLabel, + trainNumber: dep.trainNumber, + trainBadge: dep.trainBadge, + operator: dep.operator, + voie: stationStop.voie || '-', + dateIso: dep.dateIso, + dateLabel: dep.dateLabel, + perturbationType: dep.perturbationType, + perturbationDetails: dep.perturbationDetails, + stops: stops, + }); + } + }); + + arrList.sort((a, b) => a.arrivalTime.localeCompare(b.arrivalTime)); + return arrList; + }; + + // Fetch stations list from Supabase + useEffect(() => { + const fetchGares = async () => { + try { + const { data } = await supabase.from('gares').select('id, nom, type').order('nom'); + if (data && data.length > 0) { + setStationsList(data); + } else { + setStationsList([ + { id: '1', nom: 'Brazey-en-Plaine', type: 'ville' }, + { id: '2', nom: 'Lille Flandres', type: 'ville' }, + { id: '3', nom: 'Dijon', type: 'ville' }, + { id: '4', nom: 'Auxerre Saint-Gervais', type: 'ville' }, + { id: '5', nom: 'Besançon Viotte', type: 'ville' }, + { id: '6', nom: 'Nevers', type: 'ville' }, + { id: '7', nom: 'Mâcon', type: 'interurbaine' }, + { id: '8', nom: 'Chalon-sur-Saône', type: 'ville' }, + { id: '9', nom: 'Paris Gare de Lyon', type: 'ville' }, + ]); + } + } catch (err) { + console.error('Error fetching gares:', err); + } + }; + fetchGares(); + }, []); + + // Update selectedStation when URL changes + useEffect(() => { + if (stationParam) { + setSelectedStation(stationParam); + setStationQuery(stationParam); + if (stationParam.toLowerCase().includes('brazey')) { + setExpandedId('b1'); + } else if (stationParam.toLowerCase().includes('dijon')) { + setExpandedId('d4'); + } + } + }, [stationParam]); + + // Handle Station Search submit & URL update + const handleShowTimetable = (stationName: string) => { + if (!stationName) return; + setSelectedStation(stationName); + setShowDropdown(false); + + const newParams = new URLSearchParams(searchParams.toString()); + newParams.set('gare', stationName); + + const targetUrl = regionSlug + ? `/${regionSlug}/se-deplacer/prochains-departs?${newParams.toString()}` + : `/se-deplacer/prochains-departs?${newParams.toString()}`; + + router.push(targetUrl); + }; + + // Check train circulation on target date (jours_personnalises) + const checkCirculation = (item: any, targetIsoDate: string = '2026-09-20'): boolean => { + if (item.actif === false) return false; + + if (Array.isArray(item.jours_personnalises) && item.jours_personnalises.length > 0) { + const jpMatch = item.jours_personnalises.find((jp: any) => { + const d = (jp.date || jp.jour || '').slice(0, 10); + return d === targetIsoDate; + }); + if (jpMatch) return Boolean(jpMatch.circule); + } + + return false; + }; + + // Quai display logic: Ville (<= 30 min) vs Interurbaine (<= 12 hours) + const formatQuaiDisplay = (rawQuai: string | null | undefined, depTimeStr: string, stationType: string = 'ville'): string => { + if (!rawQuai || rawQuai.trim() === '' || rawQuai === '-') return '-'; + + const now = new Date(); + const nowTotalMins = now.getHours() * 60 + now.getMinutes(); + const [depH, depM] = depTimeStr.split(':').map(Number); + const depTotalMins = depH * 60 + depM; + const diffMins = depTotalMins - nowTotalMins; + + const thresholdMins = stationType === 'interurbaine' ? 720 : 30; + + if (diffMins >= 0 && diffMins <= thresholdMins) { + return rawQuai.trim(); + } + return '-'; + }; + + // Line Badge BG color helper + const getLineBadgeBg = (badge: string) => { + if (!badge) return '#006837'; + const upper = badge.toUpperCase(); + if (upper.startsWith('P')) return '#006837'; + if (upper.startsWith('C')) return '#0066a4'; + if (upper.startsWith('K')) return '#b81d77'; + return '#006837'; + }; + + // Fetch departures for selectedStation with strict perturbation date matching + useEffect(() => { + if (!selectedStation) { + setDepartures([]); + setTravauxList([]); + return; + } + + const loadDepartures = async () => { + setLoading(true); + try { + const cleanName = selectedStation.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + + // 1. Query Gares for type + const { data: gData } = await supabase.from('gares').select('id, nom, type'); + const gMap: Record = {}; + let currentStationType = 'ville'; + + if (gData) { + gData.forEach((g: any) => { + gMap[g.id] = g.nom; + const gClean = g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + if (gClean === cleanName || gClean.includes(cleanName) || cleanName.includes(gClean)) { + currentStationType = g.type || 'ville'; + } + }); + } + + // 2. Query Perturbations from Supabase + const { data: pertData } = await supabase + .from('perturbations') + .select('*') + .eq('actif', true) + .is('deleted_at', null); + + // 3. Query Travaux from Supabase + const { data: trData } = await supabase + .from('travaux') + .select('*') + .eq('actif', true) + .is('deleted_at', null); + + if (trData && trData.length > 0) { + setTravauxList(trData.map((t: any) => ({ + id: t.id?.toString(), + titre: t.titre || "Travaux d'entretien des voies", + description: (t.description || '').replace(/<[^>]*>?/gm, ''), + ligne_nom: t.ligne_nom || '', + }))); + } else { + setTravauxList([]); + } + + // 4. Query Horaires from Supabase + const { data: hData } = await supabase + .from('horaires') + .select('*') + .eq('actif', true) + .is('deleted_at', null); + + if (hData && hData.length > 0) { + const processForDate = (targetDateStr: string, isTomorrow: boolean = false) => { + const list: DepartureRow[] = []; + const allRowsForArrivals: DepartureRow[] = []; + let dateLabelStr: string | undefined = undefined; + + if (isTomorrow) { + const d = new Date(targetDateStr); + dateLabelStr = d.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' }); + dateLabelStr = dateLabelStr.charAt(0).toUpperCase() + dateLabelStr.slice(1); + } + + hData.forEach((h: any, idx: number) => { + if (!checkCirculation(h, targetDateStr)) return; + + let gares = []; + if (typeof h.gares_desservies === 'string') { + try { gares = JSON.parse(h.gares_desservies); } catch (e) {} + } else if (Array.isArray(h.gares_desservies)) { + gares = h.gares_desservies; + } + + const stopIdx = gares.findIndex((g: any) => { + const gNom = (g.gare_nom || gMap[g.gare_id] || '').toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); + return gNom.includes(cleanName) || cleanName.includes(gNom); + }); + + if (stopIdx !== -1) { + const stop = gares[stopIdx]; + const isTerminus = stop.is_arrivee || (stopIdx === gares.length - 1 && !stop.heure_depart) || !stop.heure_depart; + + const depTime = (stop.heure_depart || stop.heure_arrivee || '').slice(0, 5) || '15:00'; + const rawQuai = stop.quai || stop.quai_reel; + const quaiVal = formatQuaiDisplay(rawQuai, depTime, currentStationType); + + const matchingPert = pertData?.find((p: any) => { + const trainMatch = (p.horaire_id === h.id || String(p.horaire_id) === String(h.numero_train)); + if (!trainMatch) return false; + return isPerturbationActiveOnDate(p, targetDateStr); + }); + + let revisedTimeStr: string | undefined = undefined; + let delayTextStr: string | undefined = undefined; + let isSupp = false; + let pertTypeStr: string | undefined = undefined; + let pertDetailsStr: string | undefined = undefined; + + if (matchingPert) { + pertTypeStr = matchingPert.type_perturbation || 'information'; + pertDetailsStr = matchingPert.cause_details || matchingPert.cause || 'Information voyageurs'; + + if (matchingPert.type_perturbation === 'suppression') { + isSupp = true; + delayTextStr = `Train supprimé : ${matchingPert.cause_details || 'Incident'}`; + } else if (matchingPert.type_perturbation === 'retard' || matchingPert.duree_retard_minutes) { + const delayMins = matchingPert.duree_retard_minutes || 10; + const [hH, hM] = depTime.split(':').map(Number); + let revTotalMins = hH * 60 + hM + delayMins; + const revH = Math.floor(revTotalMins / 60) % 24; + const revM = revTotalMins % 60; + revisedTimeStr = `${String(revH).padStart(2, '0')}:${String(revM).padStart(2, '0')}`; + delayTextStr = `Retard estimé de ${delayMins} min (${matchingPert.cause_details || 'Incident'})`; + } + } + + const stopsMapped = gares.map((g: any, gIdx: number) => { + let impacts = []; + if (typeof matchingPert?.impacts_arrets === 'string') { + try { impacts = JSON.parse(matchingPert.impacts_arrets); } catch (e) {} + } else if (Array.isArray(matchingPert?.impacts_arrets)) { + impacts = matchingPert.impacts_arrets; + } + + let stopPert = undefined; + if (impacts.length > 0) { + stopPert = impacts.find((a: any) => a.gare_id === g.gare_id || a.gare_nom === g.gare_nom); + } + + return { + departTime: (g.heure_depart || g.heure_arrivee || '').slice(0, 5), + arriveeTime: (g.heure_arrivee || g.heure_depart || '').slice(0, 5), + gareNom: g.gare_nom || gMap[g.gare_id] || '', + voie: (g.quai && String(g.quai).trim() !== '') ? String(g.quai).trim() : '-', + isDepart: gIdx === 0, + isArrivee: gIdx === gares.length - 1, + perturbationType: stopPert ? stopPert.type_perturbation : undefined, + perturbationDetails: stopPert ? stopPert.cause_arret : undefined, + isNouvelleOrigine: stopPert ? stopPert.is_nouvelle_origine : false, + isNouveauTerminus: stopPert ? stopPert.is_nouveau_terminus : false, + retardMinutes: stopPert ? stopPert.retard_minutes : 0 + }; + }); + + const rowData: DepartureRow = { + id: h.id || `h-${idx}-${targetDateStr}`, + departTime: depTime, + revisedTime: revisedTimeStr, + delayText: delayTextStr, + isSupprime: isSupp, + destination: isTerminus + ? (gares[0]?.gare_nom || gMap[gares[0]?.gare_id] || 'Origine') + : (h.gare_arrivee_nom || gares[gares.length - 1]?.gare_nom || 'Terminus'), + modeIcon: (h.type_train || '').toLowerCase().includes('car') ? 'bus' : 'train', + modeLabel: h.type_train || 'Train TER', + trainNumber: h.numero_train || `89${idx + 301}`, + trainBadge: h.badge_ligne || undefined, + voie: quaiVal, + rawQuai: rawQuai, + dateIso: targetDateStr, + dateLabel: dateLabelStr, + perturbationType: pertTypeStr, + perturbationDetails: pertDetailsStr, + operator: h.operateur || 'SNCF Voyageurs - 1187', + stops: stopsMapped, + }; + + allRowsForArrivals.push(rowData); + + if (!isTerminus) { + list.push(rowData); + } + } + }); + + list.sort((a, b) => a.departTime.localeCompare(b.departTime)); + return { list, allRowsForArrivals }; + }; + + const todayIso = getTodayIso(); + let { list: todayDeps, allRowsForArrivals: todayArrRows } = processForDate(todayIso, false); + + let filteredDeps = filterPastItems(todayDeps, 'departTime'); + let genArrivals = generateArrivals(todayArrRows, selectedStation); + let filteredArr = filterPastItems(genArrivals, 'arrivalTime'); + + // If less than 20, load tomorrow + if (filteredDeps.length < 20 || filteredArr.length < 20) { + const tomDate = new Date(); + tomDate.setDate(tomDate.getDate() + 1); + const tomorrowIso = `${tomDate.getFullYear()}-${String(tomDate.getMonth() + 1).padStart(2, '0')}-${String(tomDate.getDate()).padStart(2, '0')}`; + + const { list: tomDeps, allRowsForArrivals: tomArrRows } = processForDate(tomorrowIso, true); + + if (filteredDeps.length < 20) { + filteredDeps = [...filteredDeps, ...tomDeps]; + } + if (filteredArr.length < 20) { + const tomGenArr = generateArrivals(tomArrRows, selectedStation); + filteredArr = [...filteredArr, ...tomGenArr]; + } + } + + setDepartures(filteredDeps.slice(0, 20)); + setArrivals(filteredArr.slice(0, 20)); + setLoading(false); + return; + } + + // Fallback demo datasets matching Image 1 & Image 2 + let fallbackDeps: DepartureRow[] = []; + if (cleanName.includes('brazey')) { + fallbackDeps = DEFAULT_BRAZEY_DEPARTURES; + setExpandedId('b1'); + } else if (cleanName.includes('lille')) { + fallbackDeps = DEFAULT_LILLE_DEPARTURES; + } else if (cleanName.includes('dijon')) { + fallbackDeps = DEFAULT_DIJON_DEPARTURES; + setExpandedId('d4'); + } else { + // Custom station fallback + fallbackDeps = DEFAULT_DIJON_DEPARTURES.map((d, i) => ({ + ...d, + id: `cust-${i}`, + stops: d.stops.map((st, idx) => idx === 0 ? { ...st, gareNom: selectedStation } : st) + })); + } + const filteredFallbackDeps = filterPastItems(fallbackDeps, 'departTime'); + setDepartures(filteredFallbackDeps); + const fallbackArrivals = generateArrivals(fallbackDeps, selectedStation); + const filteredFallbackArr = filterPastItems(fallbackArrivals, 'arrivalTime'); + setArrivals(filteredFallbackArr); + + } catch (err) { + console.error('Error fetching departures:', err); + setDepartures(DEFAULT_DIJON_DEPARTURES); + } finally { + setLoading(false); + } + }; + + loadDepartures(); + }, [selectedStation]); + + const handleSearchInputChange = (val: string) => { + setStationQuery(val); + if (!val.trim()) { + setFilteredOptions([]); + setShowDropdown(false); + return; + } + const filtered = stationsList.filter(s => + s.nom.toLowerCase().includes(val.toLowerCase()) + ).slice(0, 8); + setFilteredOptions(filtered); + setShowDropdown(true); + }; + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+
+ {/* Header with dynamic region theme */} + + + {/* ─── BREADCRUMB ─── */} +
+ + home + + › + + Prochains départs + + {selectedStation && ( + <> + › + Gare {selectedStation} + + )} +
+ + {/* ─── MAIN CONTENT ─── */} +
+ + {/* STEP 1: STATION SEARCH VIEW */} + {!selectedStation ? ( +
+ + {/* Green Train + Clock Pictogram */} +
+
+
+ train + access_time +
+
+
+ + {/* Title & Subtitle */} +

+ Prochains départs +

+

+ Tableaux des départs et arrivées de plus de 5000 gares +

+ + {/* Toggle Switch (Départs / Arrivées) */} +
+ + +
+ + {/* Station Search Input Box with Autocomplete */} +
+
+ search + handleSearchInputChange(e.target.value)} + onFocus={() => stationQuery && setShowDropdown(true)} + placeholder="Rechercher une gare" + className="w-full bg-transparent text-[14px] text-gray-900 outline-none placeholder:text-gray-400" + /> + {stationQuery && ( + + )} +
+ + {/* Autocomplete Dropdown */} + {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" + > + {st.nom} + chevron_right +
+ ))} +
+ )} +
+ + {/* Action Button: "Afficher les horaires" */} + + +
+ ) : ( + /* STEP 2: TIMETABLE & DETAILED HORAIRE VIEW (STRICTLY IMAGE 1 & IMAGE 2 DESIGN) */ +
+ + {/* Header Title Row (Image 1) */} +
+
+

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

+

+ Gare {selectedStation} +

+
+ + {/* Top Right Action Links (Image 1) */} +
+ + +
+
+ + {/* 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) */} +
+ + +
+ + {/* ─── MAIN TABLE (STRICTLY IMAGE 1 & IMAGE 2) ─── */} +
+ + {/* Table Header (Top yellow-green border line matching Image 1) */} +
+ {activeSubTab === 'arrivees' ? 'Arrivée' : 'Départ'} + {activeSubTab === 'arrivees' ? 'Provenance' : 'Destination'} + 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); + + // Resolve perturbation banner style + const pertStyle = dep.perturbationType + ? getPerturbationBannerStyle(dep.perturbationType) + : null; + + return ( + + + {/* Date Separator Row (Image 1: e.g. "Lundi 21 septembre") */} + {showDateHeader && ( +
+ {dep.dateLabel} +
+ )} + +
+ + {/* 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} + +
+ ) : ( + + {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} +
+ )} +
+
+ )} + +

+ 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 +
+ +
+ {/* Vertical timeline line */} +
+ + {dep.stops.map((stop, sIdx) => { + const isFirst = sIdx === 0; + return ( +
+ {/* Radio Button Column */} +
+ {!isFirst && ( +
+ )} +
+ + {/* 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 +
+
+
+ )} + +
+ + ); + }) + ) : ( +
+ 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}. +
+ )} +
+ )} + +
+ +
+ )} + +
+
+ + {/* ─── 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 +
+ +
+
+
+ ); +} diff --git a/src/components/RegionContent.tsx b/src/components/RegionContent.tsx index 168b22c..f0850a5 100644 --- a/src/components/RegionContent.tsx +++ b/src/components/RegionContent.tsx @@ -1,8 +1,13 @@ "use client"; import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { supabase } from '@/lib/supabase'; +import { getRegionTheme } from '@/lib/regionTheme'; +import SiteHeader from '@/components/SiteHeader'; +import HeaderMenu from '@/components/HeaderMenu'; +import TimetableSearchForm from '@/components/TimetableSearchForm'; interface RegionData { nom_site: string; @@ -39,6 +44,21 @@ export interface ActualiteItem { region_id?: string; } +export interface EvenementItem { + id: string | number; + titre: string; + description?: string; + lieu?: string | null; + gare_id?: string | null; + date_debut?: string; + date_fin?: string | null; + image_url?: string | null; + lien_externe?: string | null; + actif?: boolean; + visible_site?: boolean; + region_id?: string; +} + export interface HoraireGareDesservie { gare_id?: string; gare_nom: string; @@ -492,16 +512,27 @@ export default function RegionContent({ regionData, initialInfoTrafic = [], initialActualites = [], + initialEvenements = [], + initialMenuStructure = null, initialHoraires = [] }: { regionSlug: string; regionData: RegionData; initialInfoTrafic?: InfoTraficItem[]; initialActualites?: ActualiteItem[]; + initialEvenements?: EvenementItem[]; + initialMenuStructure?: any; initialHoraires?: HoraireItem[]; }) { const router = useRouter(); + // Region candidates for alias support + const regionCandidates = Array.from(new Set([ + regionSlug, + regionSlug === 'sud-provence-alpes-cote-d-azur' ? 'provence-alpes-cote-azur' : '', + regionSlug === 'provence-alpes-cote-azur' ? 'sud-provence-alpes-cote-d-azur' : '', + ].filter(Boolean))); + // Dynamic Region Configuration const config = REGION_CONFIGS[regionSlug] || REGION_CONFIGS["sud-provence-alpes-cote-d-azur"]; const regionDisplayName = regionData?.nom_site @@ -509,28 +540,16 @@ export default function RegionContent({ : config.displayName; // State: Navigation tabs - const [activeTab, setActiveTab] = useState("Tarifs et cartes de réduction"); + const [activeTab, setActiveTab] = useState("Se déplacer"); - // State: Search Booking Box - const [departureStation, setDepartureStation] = useState(""); - const [arrivalStation, setArrivalStation] = useState(""); - const [showVia, setShowVia] = useState(false); - const [viaStation, setViaStation] = useState(""); - const [outwardDate, setOutwardDate] = useState("Mer. 26 août"); - const [outwardTime, setOutwardTime] = useState("01:00"); - const [hasReturn, setHasReturn] = useState(false); - const [returnDate, setReturnDate] = useState("Jeu. 27 août"); - const [returnTime, setReturnTime] = useState("18:00"); - const [passengerCount, setPassengerCount] = useState(1); - const [selectedDiscountCard, setSelectedDiscountCard] = useState("Sans carte"); - const [showPassengerModal, setShowPassengerModal] = useState(false); + // State: Search feedback const [searchSuccessMessage, setSearchSuccessMessage] = useState(""); // State: Prochains départs const [selectedStation, setSelectedStation] = useState(config.stations[0] || "Dijon"); const [expandedDepartures, setExpandedDepartures] = useState(false); - // State: Traffic info (Default to collapsed/non-extended mode) + // State: Traffic info const [trafficPage, setTrafficPage] = useState(1); const [isTrafficExpanded, setIsTrafficExpanded] = useState(false); @@ -556,10 +575,13 @@ export default function RegionContent({ } }, [regionSlug, config.stations]); - // 1. STATE & LIVE SUPABASE SYNC (ZERO MOCK) + // 1. STATE & LIVE SUPABASE SYNC (STRICTLY REGIONAL) const [trafficDataList, setTrafficDataList] = useState(initialInfoTrafic || []); const [actusDataList, setActusDataList] = useState(initialActualites || []); + const [evenementsDataList, setEvenementsDataList] = useState(initialEvenements || []); + const [menuStructure, setMenuStructure] = useState(initialMenuStructure || null); const [horairesDataList, setHorairesDataList] = useState(initialHoraires || []); + const [garesTypeMap, setGaresTypeMap] = useState>({}); useEffect(() => { if (initialInfoTrafic && initialInfoTrafic.length > 0) { @@ -568,11 +590,17 @@ export default function RegionContent({ if (initialActualites && initialActualites.length > 0) { setActusDataList(initialActualites); } + if (initialEvenements && initialEvenements.length > 0) { + setEvenementsDataList(initialEvenements); + } + if (initialMenuStructure) { + setMenuStructure(initialMenuStructure); + } if (initialHoraires && initialHoraires.length > 0) { setHorairesDataList(initialHoraires); } - // Client-side live fetch to guarantee 100% up-to-date DB data + // Client-side live fetch filtered strictly by region_id const fetchLiveSupabase = async () => { try { // 1. Info trafic @@ -580,6 +608,7 @@ export default function RegionContent({ .from('info_trafic') .select('*') .eq('actif', true) + .in('region_id', regionCandidates) .is('deleted_at', null) .order('created_at', { ascending: false }); if (tData) { @@ -591,6 +620,7 @@ export default function RegionContent({ .from('actualites') .select('*') .eq('actif', true) + .in('region_id', regionCandidates) .is('deleted_at', null) .order('date_publication', { ascending: false }) .order('created_at', { ascending: false }); @@ -598,11 +628,39 @@ export default function RegionContent({ setActusDataList(aData as ActualiteItem[]); } - // 3. Gares & Horaires - const { data: gData } = await supabase.from('gares').select('id, nom'); + // 3. Evenements + const { data: eData } = await supabase + .from('evenements') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .is('deleted_at', null) + .order('date_debut', { ascending: false }); + if (eData) { + setEvenementsDataList(eData as EvenementItem[]); + } + + // 4. Menus régionaux + const { data: mData } = await supabase + .from('menus_regionaux') + .select('*') + .eq('actif', true) + .in('region_id', regionCandidates) + .limit(1); + if (mData && mData.length > 0) { + setMenuStructure(mData[0].structure); + } + + // 5. Gares & Horaires + const { data: gData } = await supabase.from('gares').select('id, nom, type'); const gMap: Record = {}; + const gTypeMap: Record = {}; if (gData) { - gData.forEach((g: any) => { gMap[g.id] = g.nom; }); + 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 @@ -624,19 +682,13 @@ export default function RegionContent({ }; fetchLiveSupabase(); - }, [initialInfoTrafic, initialActualites, initialHoraires, regionSlug]); + }, [initialInfoTrafic, initialActualites, initialEvenements, initialMenuStructure, initialHoraires, regionSlug]); - // Filter traffic for this region or take all active DB records - const regionSpecificTraffics = trafficDataList.filter(t => - !t.region_id || t.region_id === regionSlug || - (regionSlug === 'provence-alpes-cote-azur' && t.region_id === 'sud-provence-alpes-cote-d-azur') || - (regionSlug === 'sud-provence-alpes-cote-d-azur' && t.region_id === 'provence-alpes-cote-azur') + // Strict regional filtering: NO fallback to other regions + const trafficItems: InfoTraficItem[] = trafficDataList.filter(t => + !t.region_id || regionCandidates.includes(t.region_id) ); - const trafficItems: InfoTraficItem[] = regionSpecificTraffics.length > 0 - ? regionSpecificTraffics - : trafficDataList; - const currentTrafficIndex = (trafficPage - 1) % (trafficItems.length || 1); const currentTraffic: InfoTraficItem | null = trafficItems.length > 0 ? trafficItems[currentTrafficIndex] @@ -659,18 +711,17 @@ export default function RegionContent({ return '#e65100'; }; - // Filter actualités for this region or take all active DB records - const regionSpecificActus = actusDataList.filter(a => - !a.region_id || a.region_id === regionSlug || - (regionSlug === 'provence-alpes-cote-azur' && a.region_id === 'sud-provence-alpes-cote-d-azur') || - (regionSlug === 'sud-provence-alpes-cote-d-azur' && a.region_id === 'provence-alpes-cote-azur') + // Strict regional actualités filter + const allActusList: ActualiteItem[] = actusDataList.filter(a => + !a.region_id || regionCandidates.includes(a.region_id) ); - const allActusList: ActualiteItem[] = regionSpecificActus.length > 0 - ? regionSpecificActus - : actusDataList; + // Strict regional evenements filter + const evenementsItems: EvenementItem[] = evenementsDataList.filter(e => + !e.region_id || regionCandidates.includes(e.region_id) + ); - // 2 dernières actualités mises en avant pour le Sub-hero (BDD Supabase) + // 2 dernières actualités mises en avant pour le Sub-hero const featuredOnly = allActusList.filter(a => a.mise_en_avant); const top2Featured: ActualiteItem[] = featuredOnly.length >= 2 ? featuredOnly.slice(0, 2) @@ -693,21 +744,19 @@ export default function RegionContent({ return `${year}-${month}-${day}`; }; - // Helper: Vérification des règles de circulation UNIQUEMENT via la colonne "jours_personnalises" + // Helper: Vérification des règles de circulation (uniquement "jours_personnalises") const isTrainCirculating = (h: HoraireItem, targetDate: string = getLocalDateStr()) => { if (h.actif === false) return false; - // Uniquement la colonne "jours_personnalises" : tableau d'objets avec { date, circule: true } - if (!Array.isArray(h.jours_personnalises) || h.jours_personnalises.length === 0) { - return false; + if (Array.isArray(h.jours_personnalises) && h.jours_personnalises.length > 0) { + const customDay = h.jours_personnalises.find(jp => { + const d = jp.date || jp.jour || ''; + return d === targetDate || d.slice(0, 10) === targetDate; + }); + if (customDay) return customDay.circule === true; } - const customDay = h.jours_personnalises.find(jp => { - const d = jp.date || jp.jour || ''; - return d === targetDate || d.slice(0, 10) === targetDate; - }); - - return customDay?.circule === true; + return false; }; // Filter and resolve departures for selectedStation taking into account gares_desservies & real quais @@ -720,7 +769,16 @@ export default function RegionContent({ // 1. Vérification de la circulation du train pour aujourd'hui if (!isTrainCirculating(h, todayDateStr)) continue; - const gares = Array.isArray(h.gares_desservies) ? h.gares_desservies : []; + 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; + } // 2. Recherche dans la balise / tableau "gares_desservies" du nom de la gare affichée const stopIndex = gares.findIndex(g => { @@ -739,8 +797,18 @@ export default function RegionContent({ const lastStop = gares[gares.length - 1]; const destination = h.gare_arrivee_nom || lastStop?.gare_nom || "Terminus"; const departureTime = (stop.heure_depart || '').slice(0, 5); - // Quai réel de l'horaire pour cette gare - const quaiVal = (stop.quai && String(stop.quai).trim() !== '') ? String(stop.quai).trim() : '-'; + + // Quai réel avec filtrage selon le type de gare (30min pour ville, 12h pour interurbaine) + const rawQuaiStr = (stop.quai && String(stop.quai).trim() !== '') ? String(stop.quai).trim() : '-'; + const currentStationType = garesTypeMap[selectedStationNorm] || (selectedStationNorm.includes('dijon') ? 'ville' : 'interurbaine'); + const [depH, depM] = departureTime.split(':').map(Number); + const depTotalMins = (depH || 0) * 60 + (depM || 0); + const nowForQuai = new Date(); + const refTotalMins = nowForQuai.getHours() * 60 + nowForQuai.getMinutes(); + const diffMins = depTotalMins - refTotalMins; + const thresholdMins = currentStationType === 'interurbaine' ? 720 : 30; + const quaiVal = (rawQuaiStr !== '-' && diffMins >= 0 && diffMins <= thresholdMins) ? rawQuaiStr : '-'; + const trainType = h.type_train || 'Train TER'; const trainNum = h.numero_train || ''; @@ -783,29 +851,16 @@ export default function RegionContent({ // Tri chronologique par heure de départ stationDepartures.sort((a, b) => a.scheduledTime.localeCompare(b.scheduledTime)); - // Swap departure & arrival - const handleSwapStations = () => { - const temp = departureStation; - setDepartureStation(arrivalStation); - setArrivalStation(temp); - }; + // Filter out past departures (where scheduledTime < current time) + const nowTimeStr = (() => { + const n = new Date(); + return `${String(n.getHours()).padStart(2, '0')}:${String(n.getMinutes()).padStart(2, '0')}`; + })(); + const filteredStationDepartures = stationDepartures.filter(d => d.scheduledTime >= nowTimeStr); - // Pre-fill search from destinations + // Pre-fill route selection and navigate to search const handleSelectRoute = (from: string, to: string) => { - setDepartureStation(from); - setArrivalStation(to); - window.scrollTo({ top: 100, behavior: 'smooth' }); - }; - - // Perform search - const handleSearchSubmit = () => { - if (!departureStation || !arrivalStation) { - setSearchSuccessMessage("Veuillez renseigner la gare de départ et d'arrivée."); - setTimeout(() => setSearchSuccessMessage(""), 4000); - return; - } - setSearchSuccessMessage(`Recherche lancée pour : ${departureStation} ➔ ${arrivalStation} le ${outwardDate}`); - setTimeout(() => setSearchSuccessMessage(""), 5000); + router.push(`/${regionSlug}/horaires`); }; // Add ticket to cart @@ -814,7 +869,7 @@ export default function RegionContent({ id: Date.now(), title: `${selectedStation} ➔ ${dep.destName} (${dep.scheduledTime})`, price: dep.price, - date: outwardDate + date: new Date().toLocaleDateString('fr-FR') }; setCartItems(prev => [...prev, newItem]); setShowCartDrawer(true); @@ -832,180 +887,50 @@ export default function RegionContent({
{/* ================================================================== */} - {/* 1. HEADER (RESPONSIVE MOBILE / TABLETTE / DESKTOP) */} + {/* 1. STICKY UNIFIED SITE HEADER COMPONENT (IMAGE 1 & 2) */} {/* ================================================================== */} -
-
- - {/* Left: Burger Menu (Mobile only) + SNCF VOYAGEURS Logo + Regional Brand Logo */} -
- {/* Burger Menu Button for Mobile & Tablet */} - + - {/* SNCF Voyageurs Logo */} -
window.scrollTo({ top: 0, behavior: 'smooth' })}> - - -
- - - {/* Regional Brand Logo (Database logo_header_url or Fallback) */} - {regionData?.logo_header_url ? ( -
- {regionDisplayName} -
- ) : regionSlug === "provence-alpes-cote-azur" || regionSlug === "sud-provence-alpes-cote-d-azur" ? ( -
- TER - Z - OU - ! -
- ) : regionSlug === "bourgogne-franche-comte" ? ( -
-
- - mobigo - - -
- - {config.tagline} - -
- ) : regionSlug === "bretagne" ? ( -
- Breizh - Go -
- ) : regionSlug === "centre-val-de-loire" ? ( -
- Ré - mi -
- ) : regionSlug === "occitanie" ? ( -
- liO - Train -
+ {/* ================================================================== */} + {/* 2. BANDEAU JAUNE ÉVÉNEMENT (CONFORME MAQUETTE IMAGE 1) */} + {/* ================================================================== */} + {evenementsItems.length > 0 && ( +
+
+ campaign + + {evenementsItems[0].titre?.toLowerCase().includes('patrimoine') + ? "Journées du patrimoine : allez-y en train, bus et car avec le pass 6€ !" + : `${evenementsItems[0].titre}${evenementsItems[0].description ? ` : ${evenementsItems[0].description}` : ''}`} + + {evenementsItems[0].lien_externe ? ( + + Billets en vente ici en nombre limité › + ) : ( -
- - {config.brandPrimary} - - - {config.brandSecondary} - -
+ + Billets en vente ici en nombre limité › + )}
- - {/* Right: Actions */} -
- {/* Desktop only: Commande */} - - -
- - {/* Panier (Mobile & Desktop) */} - - -
- - {/* Compte (Mobile & Desktop) */} - -
-
- - {/* ================================================================== */} - {/* 2. NAVIGATION BAR (DESKTOP & GRANDES TABLETTES) */} - {/* ================================================================== */} - + )} {/* ================================================================== */} {/* 3. HERO & SEARCH BOX (RESPONSIVE CONFORME IMAGE 1) */} @@ -1035,165 +960,11 @@ export default function RegionContent({
- {/* ─── BLOC DE RECHERCHE D'HORAIRES (SCALE EXACTE IMAGE 1) ─── */} -
- - {/* 1. Gare de départ */} -
-
- - place - - setDepartureStation(e.target.value)} - placeholder="Gare de départ" - className="w-full bg-transparent text-[14px] text-gray-800 placeholder-gray-500 outline-none font-normal" - /> -
- -
- - {/* 2. Gare d'arrivée */} -
- - place - - setArrivalStation(e.target.value)} - placeholder="Gare d'arrivée" - className="w-full bg-transparent text-[14px] text-gray-800 placeholder-gray-500 outline-none font-normal" - /> -
- - {/* + Via */} -
- -
- - {showVia && ( -
- alt_route - setViaStation(e.target.value)} - placeholder="Gare de passage (Via)" - className="w-full bg-transparent text-[13.5px] text-gray-800 placeholder-gray-500 outline-none" - /> -
- )} - - {/* 3. Aller */} -
-
- - calendar_today - -
- Aller - setOutwardDate(e.target.value)} - className="bg-transparent text-[13.5px] font-semibold text-gray-900 leading-tight outline-none w-28" - /> -
-
-
- setOutwardTime(e.target.value)} - className="w-14 bg-transparent text-left font-normal text-[14px] text-gray-900 outline-none" - /> -
- - {/* 4. Retour */} -
setHasReturn(!hasReturn)} - className="bg-[#f2f2f3] rounded-lg px-3.5 py-2.5 mb-2.5 flex items-center justify-between hover:bg-[#eaeaea] transition-colors cursor-pointer" - > -
- - calendar_today - - - {hasReturn ? returnDate : "Retour (optionnel)"} - -
-
-
- {hasReturn ? returnTime : "--:--"} -
-
- - {/* 5. Voyageurs */} -
setShowPassengerModal(true)} - className="bg-[#f2f2f3] rounded-lg px-3.5 py-2.5 mb-4 sm:mb-5 flex items-center justify-between hover:bg-[#eaeaea] transition-colors cursor-pointer" - > -
- - account_circle - - - {passengerCount} Voyageur{passengerCount > 1 ? 's' : ''}, {selectedDiscountCard} - -
- - add - -
- - {/* 6. Rechercher Button (Full width on mobile, right on desktop) */} -
- -
-
+ {/* ─── REUSABLE TIMETABLE SEARCH FORM MODULE (IMAGE 1) ─── */} + setSearchSuccessMessage(`Recherche d'horaire lancée de "${params.departure || 'Gare'}" à "${params.arrival || 'Gare'}" pour le ${params.outwardDate}`)} + /> {/* ─── SIDE COLUMN (INFO TRAFIC + S'ABONNER) ─── */}
@@ -1486,7 +1257,7 @@ export default function RegionContent({ id: Date.now(), title: `Billet ${selectedStation} → ${dep.destName}`, price: 12.00, - date: outwardDate + date: new Date().toLocaleDateString('fr-FR') }]); setShowCartDrawer(true); }} @@ -1690,6 +1461,53 @@ export default function RegionContent({
+ {/* ================================================================== */} + {/* 5.5 ÉVÉNEMENTS RÉGIONAUX (BDD SUPABASE) */} + {/* ================================================================== */} + {evenementsItems.length > 0 && ( +
+
+

+ Événements en {regionDisplayName} +

+
+ {evenementsItems.map((ev, idx) => ( +
+
+ {ev.image_url && ( +
+ {ev.titre} +
+ )} +

{ev.titre}

+ {ev.date_debut && ( +

+ Du {new Date(ev.date_debut).toLocaleDateString('fr-FR')} {ev.date_fin ? `au ${new Date(ev.date_fin).toLocaleDateString('fr-FR')}` : ''} +

+ )} + {ev.description && ( +
+ +
+ )} +
+ {ev.lien_externe && ( + + En savoir plus ↗ + + )} +
+ ))} +
+
+
+ )} + {/* ================================================================== */} {/* 6. DESTINATIONS PLÉBISCITÉES */} {/* ================================================================== */} @@ -1837,60 +1655,6 @@ export default function RegionContent({ {/* 8. MODALES & TIROIRS */} {/* ================================================================== */} - {/* MODAL PASSAGERS */} - {showPassengerModal && ( -
-
-

Voyageurs et Cartes

- -
- -
- - {passengerCount} - -
-
- -
- - -
- -
- -
-
-
- )} - {/* DRAWER PANIER */} {showCartDrawer && (
diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx new file mode 100644 index 0000000..dd7c41b --- /dev/null +++ b/src/components/SiteHeader.tsx @@ -0,0 +1,297 @@ +"use client"; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import HeaderMenu, { MenuItem } from '@/components/HeaderMenu'; +import { getRegionTheme } from '@/lib/regionTheme'; + +interface SiteHeaderProps { + regionSlug?: string; + logoUrl?: string; + brandPrimary?: string; + brandSecondary?: string; + menuStructure?: MenuItem[] | null; + activeTab?: string; + onTabSelect?: (label: string) => void; +} + +const REGION_LOGOS: Record = { + 'bourgogne-franche-comte': 'https://www.ter.sncf.com/assets/medias/bfc/headerLogo.svg', + 'occitanie': 'https://www.ter.sncf.com/assets/medias/occitanie/headerLogo.svg', + 'provence-alpes-cote-azur': 'https://www.ter.sncf.com/assets/medias/paca/headerLogo.svg', + 'sud-provence-alpes-cote-d-azur': 'https://www.ter.sncf.com/assets/medias/paca/headerLogo.svg', + 'bretagne': 'https://www.ter.sncf.com/assets/medias/bretagne/headerLogo.svg', + 'grand-est': 'https://www.ter.sncf.com/assets/medias/ge/headerLogo.svg', + 'normandie': 'https://www.ter.sncf.com/assets/medias/normandie/headerLogo.svg', + 'hauts-de-france': 'https://www.ter.sncf.com/assets/medias/hdf/headerLogo.svg', + 'pays-de-la-loire': 'https://www.ter.sncf.com/assets/medias/aleop/headerLogo.svg', + 'nouvelle-aquitaine': 'https://www.ter.sncf.com/assets/medias/na/headerLogo.svg', + 'centre-val-de-loire': 'https://www.ter.sncf.com/assets/medias/cen/headerLogo.svg', + 'auvergne-rhone-alpes': 'https://www.ter.sncf.com/assets/medias/aura/headerLogo.svg', +}; + +export default function SiteHeader({ + regionSlug = 'bourgogne-franche-comte', + logoUrl, + brandPrimary, + brandSecondary, + menuStructure, + activeTab, + onTabSelect, +}: SiteHeaderProps) { + const theme = getRegionTheme(regionSlug); + const [showOrderModal, setShowOrderModal] = useState(false); + const [showCartDrawer, setShowCartDrawer] = useState(false); + const [showAccountModal, setShowAccountModal] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const effectiveLogoUrl = logoUrl || REGION_LOGOS[regionSlug] || REGION_LOGOS['bourgogne-franche-comte']; + + return ( +
+ {/* ─── TOP BRANDING & ACTIONS BAR (CONFORME IMG 2) ─── */} +
+ + {/* Left: Mobile Hamburger Icon (Img 2) & Logos */} +
+ + + + {effectiveLogoUrl ? ( + Logo TER Région + ) : ( +
+ + SNCF + + + TER + +
+ )} + + {(brandPrimary || brandSecondary) && ( +
+ + {brandPrimary || "RÉGION"} + + + {brandSecondary || "BOURGOGNE-FRANCHE-COMTÉ"} + +
+ )} + +
+ + {/* Right: Actions (Commande, Panier, Compte - Img 2) */} +
+ + {/* Commande */} + + +
+ + {/* Panier */} + + +
+ + {/* Compte */} + +
+
+ + {/* ─── DYNAMIC HEADER MENU (ATTACHED TO HEADER & STICKY) ─── */} + + + {/* MODALE COMMANDE */} + {showOrderModal && ( +
setShowOrderModal(false)} + > +
e.stopPropagation()} + > +
+

Suivi & Gestion de Commande

+ +
+

+ Saisissez votre numéro de commande pour consulter vos billets et justificatifs de voyage. +

+ +
+ + +
+
+
+ )} + + {/* DRAWER PANIER */} + {showCartDrawer && ( +
setShowCartDrawer(false)} + > +
e.stopPropagation()} + > +
+
+
+ shopping_bag +

Votre Panier

+
+ +
+
+ remove_shopping_cart + Votre panier est vide pour le moment. +
+
+ +
+
+ )} + + {/* MODALE COMPTE */} + {showAccountModal && ( +
setShowAccountModal(false)} + > +
e.stopPropagation()} + > +
+

Espace Mon Compte

+ +
+
+ + +
+ +
+
+ )} + +
+ ); +} diff --git a/src/components/TimetableSearchForm.tsx b/src/components/TimetableSearchForm.tsx new file mode 100644 index 0000000..4479a97 --- /dev/null +++ b/src/components/TimetableSearchForm.tsx @@ -0,0 +1,810 @@ +"use client"; + +import React, { useState, useEffect, useRef } from 'react'; +import { useRouter, usePathname } from 'next/navigation'; +import { supabase } from '@/lib/supabase'; + +type ActiveField = 'departure' | 'arrival' | 'via' | 'outwardDate' | 'outwardTime' | 'returnDate' | 'returnTime' | 'passenger' | null; + +export interface PassengerInfo { + id: number; + age: string; + discountCard: string; +} + +interface TimetableSearchFormProps { + initialDeparture?: string; + initialArrival?: string; + regionSlug?: string; + onSearch?: (searchParams: { + departure: string; + arrival: string; + via: string; + outwardDate: string; + outwardTime: string; + hasReturn: boolean; + returnDate: string; + returnTime: string; + passengerCount: number; + discountCard: string; + }) => void; + className?: string; +} + +const DEFAULT_STATIONS = [ + 'Dijon', + 'Dijon (Toutes Gares)', + 'Dijon Porte Neuve', + 'Neuilly-lès-Dijon', + 'Aiserey', + 'Bourg-en-Bresse', + 'Brazey-en-Plaine', + 'Chaugey', + 'Longecourt', + 'Louhans', + 'Mervans', + 'Ouges', + 'Pagny', + 'Saint-Amour', + 'Saint-Jean-de-Losne', + 'Saulon', + 'Seurre', + 'Paris Gare de Lyon', + 'Paris Montparnasse', + 'Paris Est', + 'Paris Nord', + 'Lyon Part-Dieu', + 'Lyon Perrache', + 'Marseille Saint-Charles', + 'Toulouse Matabiau', + 'Nice Ville', + 'Rennes', + 'Nantes', + 'Bordeaux Saint-Jean', + 'Strasbourg', + 'Lille Flandres' +]; + +const DISCOUNT_CARDS = [ + 'Carte de réduction (optionnel)', + 'Sans carte', + 'Carte Avantage Jeune (12-27 ans)', + 'Carte Avantage Adulte (27-59 ans)', + 'Carte Avantage Senior (60+ ans)', + 'Abonnement Régional Mobigo', + 'Carte Liberté', + 'Famille Nombreuse' +]; + +const TIME_SLOTS = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`); + +export default function TimetableSearchForm({ + initialDeparture = '', + initialArrival = '', + regionSlug: propRegionSlug, + onSearch, + className = '', +}: TimetableSearchFormProps) { + const router = useRouter(); + const pathname = usePathname(); + const containerRef = useRef(null); + + // Determine current region slug from props or URL pathname + const currentRegionSlug = propRegionSlug || (() => { + if (!pathname) return ''; + const parts = pathname.split('/').filter(Boolean); + const knownNonRegion = ['horaires', 'offers', 'se-deplacer', 'abonnements']; + if (parts.length > 0 && !knownNonRegion.includes(parts[0])) { + return parts[0]; + } + return ''; + })(); + + // Form states + const [departureStation, setDepartureStation] = useState(initialDeparture); + const [arrivalStation, setArrivalStation] = useState(initialArrival); + const [viaStation, setViaStation] = useState(''); + const [showVia, setShowVia] = useState(false); + const [outwardDate, setOutwardDate] = useState('Dim. 20 sept.'); + const [outwardTime, setOutwardTime] = useState('16:00'); + const [hasReturn, setHasReturn] = useState(false); + const [returnDate, setReturnDate] = useState('Dim. 20 sept.'); + const [returnTime, setReturnTime] = useState('--:--'); + + // Interactive active field panel state + const [activeField, setActiveField] = useState(null); + + // Station database items + const [allStations, setAllStations] = useState(DEFAULT_STATIONS); + + // Calendar state for date picker (default September 20, 2026) + const [calendarDate, setCalendarDate] = useState(new Date(2026, 8, 20)); // Month 8 is Sept + const [selectedDayNumber, setSelectedDayNumber] = useState(20); + + // Passenger state matching Img 1 + const [passengers, setPassengers] = useState([ + { id: 1, age: '', discountCard: 'Carte de réduction (optionnel)' } + ]); + + // Fetch stations from Supabase + useEffect(() => { + const fetchGares = async () => { + try { + const { data } = await supabase.from('gares').select('nom').order('nom'); + if (data && data.length > 0) { + const names = data.map((g: any) => g.nom); + const combined = Array.from(new Set([...DEFAULT_STATIONS, ...names])); + setAllStations(combined); + } + } catch (err) { + console.error('Error fetching gares:', err); + } + }; + fetchGares(); + }, []); + + // Close side panel on click outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setActiveField(null); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSwapStations = (e: React.MouseEvent) => { + e.stopPropagation(); + const temp = departureStation; + setDepartureStation(arrivalStation); + setArrivalStation(temp); + }; + + const handleAddPassenger = () => { + setPassengers(prev => [ + ...prev, + { id: prev.length + 1, age: '', discountCard: 'Carte de réduction (optionnel)' } + ]); + }; + + const handleRemovePassenger = (id: number) => { + if (passengers.length <= 1) return; + setPassengers(prev => prev.filter(p => p.id !== id).map((p, idx) => ({ ...p, id: idx + 1 }))); + }; + + const handleUpdatePassenger = (id: number, key: 'age' | 'discountCard', val: string) => { + setPassengers(prev => prev.map(p => p.id === id ? { ...p, [key]: val } : p)); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setActiveField(null); + + const firstPassenger = passengers[0]; + const discountStr = firstPassenger?.discountCard !== 'Carte de réduction (optionnel)' + ? firstPassenger.discountCard + : 'Sans carte'; + + if (onSearch) { + onSearch({ + departure: departureStation || 'Dijon', + arrival: arrivalStation || 'Seurre', + via: showVia ? viaStation : '', + outwardDate, + outwardTime, + hasReturn, + returnDate, + returnTime, + passengerCount: passengers.length, + discountCard: discountStr, + }); + } + + const queryParams = new URLSearchParams({ + departure: departureStation || 'Dijon', + arrival: arrivalStation || 'Seurre', + via: showVia ? viaStation : '', + outwardDate, + outwardTime, + hasReturn: hasReturn ? '1' : '0', + returnDate: hasReturn ? returnDate : '', + returnTime: hasReturn ? returnTime : '', + passengers: passengers.length.toString(), + passengerAge: firstPassenger?.age || '30', + discountCard: discountStr, + }); + + const targetUrl = currentRegionSlug + ? `/${currentRegionSlug}/offers?${queryParams.toString()}` + : `/offers?${queryParams.toString()}`; + + router.push(targetUrl); + }; + + // Get matching stations for active text input + const getActiveInputValue = () => { + if (activeField === 'departure') return departureStation; + if (activeField === 'arrival') return arrivalStation; + if (activeField === 'via') return viaStation; + return ''; + }; + + const currentQuery = getActiveInputValue().trim(); + + const filteredStations = allStations.filter(st => + st.toLowerCase().includes(currentQuery.toLowerCase()) + ); + + const handleSelectStation = (stName: string) => { + if (activeField === 'departure') setDepartureStation(stName); + if (activeField === 'arrival') setArrivalStation(stName); + if (activeField === 'via') setViaStation(stName); + setActiveField(null); + }; + + // Calendar calculations + const year = calendarDate.getFullYear(); + const month = calendarDate.getMonth(); + const monthName = calendarDate.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const firstDayOfWeek = (new Date(year, month, 1).getDay() + 6) % 7; // Mon = 0 + + const handlePrevMonth = () => { + setCalendarDate(new Date(year, month - 1, 1)); + }; + + const handleNextMonth = () => { + setCalendarDate(new Date(year, month + 1, 1)); + }; + + const handleSelectDay = (dayNum: number) => { + setSelectedDayNumber(dayNum); + const d = new Date(year, month, dayNum); + const daysShort = ['Dim.', 'Lun.', 'Mar.', 'Mer.', 'Jeu.', 'Ven.', 'Sam.']; + const monthsShort = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.']; + const formatted = `${daysShort[d.getDay()]} ${dayNum} ${monthsShort[d.getMonth()]}`; + + if (activeField === 'outwardDate') { + setOutwardDate(formatted); + } else if (activeField === 'returnDate') { + setReturnDate(formatted); + setHasReturn(true); + } + setActiveField(null); + }; + + const handleSelectTime = (slot: string) => { + if (activeField === 'outwardTime') { + setOutwardTime(slot); + } else if (activeField === 'returnTime') { + setReturnTime(slot); + setHasReturn(true); + } + setActiveField(null); + }; + + // Pointer position top offset calculation matching the input row centers (Img 2 & Img 1) + const getPointerTop = () => { + switch (activeField) { + case 'departure': return 'top-6'; + case 'arrival': return 'top-20'; + case 'via': return 'top-28'; + case 'outwardDate': + case 'outwardTime': return 'top-40'; + case 'returnDate': + case 'returnTime': return 'top-56'; + case 'passenger': return 'top-[290px]'; + default: return 'top-6'; + } + }; + + return ( +
+ + {/* ================================================================== */} + {/* 1. LEFT SEARCH FORM CARD (CONFORME IMG 1 & 2) */} + {/* ================================================================== */} +
+
+ + {/* 1. Gare de départ */} +
setActiveField('departure')} + 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' + : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' + }`} + > +
+ + place + + { + setDepartureStation(e.target.value); + if (activeField !== 'departure') setActiveField('departure'); + }} + onFocus={() => setActiveField('departure')} + placeholder="Gare de départ" + className="w-full bg-transparent text-[14px] text-gray-800 placeholder-gray-500 outline-none font-medium" + /> +
+ +
+ + {/* 2. Gare d'arrivée */} +
setActiveField('arrival')} + 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' + : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' + }`} + > + + place + + { + setArrivalStation(e.target.value); + if (activeField !== 'arrival') setActiveField('arrival'); + }} + onFocus={() => setActiveField('arrival')} + placeholder="Gare d'arrivée" + className="w-full bg-transparent text-[14px] text-gray-800 placeholder-gray-500 outline-none font-medium" + /> +
+ + {/* + Via link */} +
+ +
+ + {showVia && ( +
setActiveField('via')} + 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]' + : 'bg-[#f2f3f5] hover:bg-[#eaecef]' + }`} + > + alt_route + setViaStation(e.target.value)} + onFocus={() => setActiveField('via')} + placeholder="Gare de passage (Via)" + className="w-full bg-transparent text-[13.5px] text-gray-800 placeholder-gray-500 outline-none" + /> +
+ )} + + {/* 3. Aller */} +
+
setActiveField('outwardDate')} + className="flex items-center gap-3 flex-1 cursor-pointer" + > + + event + +
+ Aller + + {outwardDate} + +
+
+ +
+ +
setActiveField('outwardTime')} + className="w-14 text-left font-normal text-[14px] text-gray-900 cursor-pointer hover:text-[#006837]" + > + {outwardTime} +
+
+ + {/* 4. Retour */} +
+
{ + setHasReturn(true); + setActiveField('returnDate'); + }} + className="flex items-center gap-3 flex-1 cursor-pointer" + > + + event + + + {hasReturn ? returnDate : "Retour (optionnel)"} + +
+ +
+ +
+
{ + setHasReturn(true); + setActiveField('returnTime'); + }} + className="w-14 text-left font-normal text-[13.5px] text-gray-700 cursor-pointer hover:text-[#006837]" + > + {hasReturn ? returnTime : "--:--"} +
+ + {hasReturn && ( + + )} +
+
+ + {/* 5. Voyageurs (CONFORME IMG 1) */} +
setActiveField('passenger')} + 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' + : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' + }`} + > +
+ + person + + + {passengers.length} Voyageur{passengers.length > 1 ? 's' : ''} + {passengers[0]?.age ? `, ${passengers[0].age} ans` : ''} + {passengers[0]?.discountCard && passengers[0].discountCard !== 'Carte de réduction (optionnel)' ? `, ${passengers[0].discountCard}` : ', sans carte'} + +
+ + add + +
+ + {/* 6. Action Button */} +
+ +
+ +
+
+ + {/* ================================================================== */} + {/* 2. DYNAMIC SIDE PANEL CARD (CONFORME IMG 1, 2, 3, 4, 5) */} + {/* ================================================================== */} + {activeField && ( +
+ {/* Left Pointer Arrow (Speech Bubble Triangle) matching Img 1 & 2 */} +
+ + {/* ─── CASE A: STATION SELECTION (GARE DE DÉPART / ARRIVÉE / VIA) (IMG 2 & 3) ─── */} + {(activeField === 'departure' || activeField === 'arrival' || activeField === 'via') && ( +
+

+ {activeField === 'departure' ? "Gare de départ" : activeField === 'arrival' ? "Gare d'arrivée" : "Gare de passage"} +

+ + {currentQuery.length < 2 ? ( +
+

+ Veuillez saisir au moins 2 caractères dans ce champ. +

+
+
+ ) : ( +
+

+ Veuillez sélectionner une gare dans cette liste. +

+
+ +
+ {filteredStations.length > 0 ? ( + filteredStations.map((st, idx) => { + const isFirst = idx === 0; + return ( +
handleSelectStation(st)} + 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' + }`} + > + {st} +
+ ); + }) + ) : ( +
+ Aucune gare ne correspond à "{currentQuery}" +
+ )} +
+
+ )} +
+ )} + + {/* ─── CASE B: DATE PICKER (ALLER / RETOUR) (IMG 4) ─── */} + {(activeField === 'outwardDate' || activeField === 'returnDate') && ( +
+

+ {activeField === 'outwardDate' ? "Date de départ Aller" : "Date de départ Retour"} +

+ +

+ Format attendu : "Jour/Mois/Année complète" (exemple: 19/09/2026). +

+ + {/* Month navigation */} +
+ + {monthName} + +
+ + +
+
+ + {/* Calendar Grid */} +
+ {['L', 'M', 'M', 'J', 'V', 'S', 'D'].map((d, i) => ( + + {d} + + ))} +
+ +
+ {/* Blank days for month start alignment */} + {Array.from({ length: firstDayOfWeek }).map((_, i) => ( +
+ ))} + + {/* Days of month */} + {Array.from({ length: daysInMonth }, (_, i) => i + 1).map((dayNum) => { + const isSelected = dayNum === selectedDayNumber; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const dayDate = new Date(year, month, dayNum, 0, 0, 0, 0); + const isPastDate = dayDate < today; + + return ( + + ); + })} +
+
+ )} + + {/* ─── CASE C: TIME PICKER (ALLER / RETOUR) (IMG 5) ─── */} + {(activeField === 'outwardTime' || activeField === 'returnTime') && ( +
+

+ {activeField === 'outwardTime' ? "Heure de départ" : "Heure de retour"} +

+ +
+ {TIME_SLOTS.map((slot) => { + const currentSelected = activeField === 'outwardTime' ? outwardTime : returnTime; + const isSelected = slot === currentSelected; + + const now = new Date(); + const isTodaySelected = (year === now.getFullYear() && month === now.getMonth() && selectedDayNumber === now.getDate()); + const slotHour = parseInt(slot.split(':')[0], 10); + const isPastTime = isTodaySelected && slotHour < now.getHours(); + + return ( +
!isPastTime && handleSelectTime(slot)} + 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' + }`} + title={isPastTime ? 'Heure passée' : undefined} + > + {slot} +
+ ); + })} +
+
+ )} + + {/* ─── CASE D: VOYAGEUR VOLET (STRICTLY CONFORME IMG 1) ─── */} + {activeField === 'passenger' && ( +
+

+ Voyageur +

+ +
+ {passengers.map((p, index) => ( +
+
+
+ + person + + + Voyageur {index + 1} + +
+ {passengers.length > 1 && ( + + )} +
+ + {/* Field 1: Âge (optionnel) - Box with dashed green border matching Image 1 */} +
+ +
+ handleUpdatePassenger(p.id, 'age', e.target.value)} + placeholder="Ex: 30" + className="w-full bg-transparent text-[14px] font-semibold text-gray-900 outline-none" + /> +
+ unfold_more +
+
+
+ + {/* Field 2: Carte de réduction (optionnel) - Select matching Image 1 */} +
+ +
+
+ ))} +
+ + {/* Yellow Horizontal Divider Line (CONFORME IMG 1) */} +
+ + {/* + Ajouter un voyageur Green Button (CONFORME IMG 1) */} +
+ +
+ +
+ )} + +
+ )} + +
+ ); +} + diff --git a/src/lib/perturbationUtils.ts b/src/lib/perturbationUtils.ts new file mode 100644 index 0000000..1c021a2 --- /dev/null +++ b/src/lib/perturbationUtils.ts @@ -0,0 +1,113 @@ +/** + * Utility functions for perturbation date matching & UI banner styling (matching Image 1 & Image 2) + */ + +export interface PerturbationData { + id?: string | number; + horaire_id?: string | number; + type_perturbation: 'retard' | 'suppression' | 'information' | 'parcours_modifie' | string; + duree_retard_minutes?: number; + cause?: string; + cause_details?: string; + date_debut?: string; + date_fin?: string; + jours_perturbation?: any[]; + actif?: boolean; +} + +/** + * Checks if a perturbation strictly applies to the recorded target ISO date (YYYY-MM-DD) + */ +export function isPerturbationActiveOnDate(pert: any, targetDateIso: string): boolean { + if (!pert || pert.actif === false) return false; + + // 1. Check jours_perturbation array if present + if (Array.isArray(pert.jours_perturbation) && pert.jours_perturbation.length > 0) { + return pert.jours_perturbation.some((jp: any) => { + if (typeof jp === 'string') { + return jp.slice(0, 10) === targetDateIso; + } + if (jp && typeof jp === 'object') { + const d = (jp.date || jp.dateDebut || jp.jour || '').slice(0, 10); + return d === targetDateIso; + } + return false; + }); + } + + // 2. Check date_debut and date_fin + const start = pert.date_debut ? pert.date_debut.slice(0, 10) : null; + const end = pert.date_fin ? pert.date_fin.slice(0, 10) : null; + + if (start && end) { + return targetDateIso >= start && targetDateIso <= end; + } + if (start) { + return targetDateIso === start; + } + if (end) { + return targetDateIso === end; + } + + // Strictly assign perturbations only when target date matches recorded date + return false; +} + +export interface BannerStyle { + bgColor: string; // Background color matching Image 2 + textColor: string; // Text color + iconBgColor: string; // Icon circle bg color + iconText: string; // Icon character ('i', '!', '✕', '↔') + badgeLabel: string; // Label ('Information', 'Informations', 'Retard', 'Suppression', 'Parcours modifié') + defaultCause?: string; +} + +/** + * Gets banner styling and metadata strictly matching Image 2 + */ +export function getPerturbationBannerStyle(type: string): BannerStyle { + const normType = (type || '').toLowerCase(); + + if (normType.includes('retard')) { + return { + bgColor: '#fff1ed', + textColor: '#c00', + iconBgColor: '#e65100', + iconText: '!', + badgeLabel: 'Retard', + defaultCause: 'Retard estimé' + }; + } + + if (normType.includes('suppr')) { + return { + bgColor: '#fdeded', + textColor: '#c00', + iconBgColor: '#c00', + iconText: '✕', + badgeLabel: 'Suppression', + defaultCause: 'Train supprimé' + }; + } + + if (normType.includes('parcours') || normType.includes('modifi')) { + return { + bgColor: '#f0f7ff', + textColor: '#0066a4', + iconBgColor: '#0066a4', + iconText: '↔', + badgeLabel: 'Parcours modifié', + defaultCause: 'Modification de parcours' + }; + } + + // Default: Information + return { + bgColor: '#eef7fc', + textColor: '#0066a4', + iconBgColor: '#0066a4', + iconText: 'i', + badgeLabel: 'Informations', + defaultCause: 'Informations voyageurs' + }; +} diff --git a/src/lib/regionTheme.ts b/src/lib/regionTheme.ts new file mode 100644 index 0000000..249ba4d --- /dev/null +++ b/src/lib/regionTheme.ts @@ -0,0 +1,99 @@ +export interface RegionTheme { + primary: string; + primaryHover: string; + primaryLight: string; + secondary: string; + badgeText: string; +} + +export const REGION_THEMES: Record = { + 'bourgogne-franche-comte': { + primary: '#006837', + primaryHover: '#00522b', + primaryLight: '#eef5e6', + secondary: '#8da300', + badgeText: 'TER Bourgogne-Franche-Comté', + }, + 'occitanie': { + primary: '#c0001a', + primaryHover: '#990014', + primaryLight: '#fde8ea', + secondary: '#e60000', + badgeText: 'liO Train Occitanie', + }, + 'provence-alpes-cote-azur': { + primary: '#005088', + primaryHover: '#003a63', + primaryLight: '#e5f0f8', + secondary: '#009fe3', + badgeText: 'ZOU ! TER PACA', + }, + 'sud-provence-alpes-cote-d-azur': { + primary: '#005088', + primaryHover: '#003a63', + primaryLight: '#e5f0f8', + secondary: '#009fe3', + badgeText: 'ZOU ! TER PACA', + }, + 'bretagne': { + primary: '#0077b6', + primaryHover: '#005b8c', + primaryLight: '#e6f4fa', + secondary: '#0096c7', + badgeText: 'TER BreizhGo', + }, + 'grand-est': { + primary: '#2b5c8f', + primaryHover: '#1e4267', + primaryLight: '#eaf0f6', + secondary: '#4885c1', + badgeText: 'TER Fluo Grand Est', + }, + 'normandie': { + primary: '#d32f2f', + primaryHover: '#b71c1c', + primaryLight: '#fbe9e7', + secondary: '#e53935', + badgeText: 'TER Nomad Normandie', + }, + 'hauts-de-france': { + primary: '#004b87', + primaryHover: '#003663', + primaryLight: '#e5edf5', + secondary: '#0066b3', + badgeText: 'TER Hauts-de-France', + }, + 'pays-de-la-loire': { + primary: '#008459', + primaryHover: '#006343', + primaryLight: '#e6f5ef', + secondary: '#00a872', + badgeText: 'TER Aléop', + }, + 'nouvelle-aquitaine': { + primary: '#0066a4', + primaryHover: '#004c7a', + primaryLight: '#e6f2fa', + secondary: '#0085d0', + badgeText: 'TER Nouvelle-Aquitaine', + }, + 'centre-val-de-loire': { + primary: '#008080', + primaryHover: '#006666', + primaryLight: '#e6f2f2', + secondary: '#00a3a3', + badgeText: 'TER Rémi', + }, + 'auvergne-rhone-alpes': { + primary: '#00609c', + primaryHover: '#004875', + primaryLight: '#e6f1f8', + secondary: '#007bc7', + badgeText: 'TER Auvergne-Rhône-Alpes', + }, +}; + +export function getRegionTheme(slug: string = 'bourgogne-franche-comte'): RegionTheme { + const cleanSlug = (slug || 'bourgogne-franche-comte').toLowerCase(); + return REGION_THEMES[cleanSlug] || REGION_THEMES['bourgogne-franche-comte']; +}