-
- {/* 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 ? (
-
-

-
- ) : 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.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 ? (
+

+ ) : (
+
+
+ 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) */}
+ {/* ================================================================== */}
+
+
+ {/* ================================================================== */}
+ {/* 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'];
+}