This commit is contained in:
mathis committed 2026-09-23 23:26:29 +02:00
1 parent 24cb502770
commit aaec7e8d0f
30 files changed
+5347 -1978

No files matched your search

+5
View File
@@ -5,3 +5,8 @@ const nextConfig: NextConfig = {
}; };
export default nextConfig; export default nextConfig;
// next.config.js
module.exports = {
allowedDevOrigins: ['192.168.1.104'],
}
+25
View File
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import FichesHorairesPageClient from '@/components/FichesHorairesPageClient';
interface RegionFichesHorairesAliasPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionFichesHorairesAliasPage({ params }: RegionFichesHorairesAliasPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des fiches horaires...</p>
</div>
</div>
}>
<FichesHorairesPageClient regionSlug={region} />
</Suspense>
);
}
+25
View File
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import GaresPageClient from '@/components/GaresPageClient';
interface RegionGaresAliasPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionGaresAliasPage({ params }: RegionGaresAliasPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de la gare...</p>
</div>
</div>
}>
<GaresPageClient regionSlug={region} />
</Suspense>
);
}
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import FichesHorairesPageClient from '@/components/FichesHorairesPageClient';
interface RegionFichesHorairesPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionFichesHorairesPage({ params }: RegionFichesHorairesPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des fiches horaires...</p>
</div>
</div>
}>
<FichesHorairesPageClient regionSlug={region} />
</Suspense>
);
}
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import GaresPageClient from '@/components/GaresPageClient';
interface RegionGaresPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionGaresPage({ params }: RegionGaresPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de la gare...</p>
</div>
</div>
}>
<GaresPageClient regionSlug={region} />
</Suspense>
);
}
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import InfoGrevesClient from '@/components/InfoGrevesClient';
interface RegionInfoGrevesPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionInfoGrevesPage({ params }: RegionInfoGrevesPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des informations grève...</p>
</div>
</div>
}>
<InfoGrevesClient regionSlug={region} />
</Suspense>
);
}
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import InfoTravauxClient from '@/components/InfoTravauxClient';
interface RegionInfoTravauxPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionInfoTravauxPage({ params }: RegionInfoTravauxPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des informations travaux...</p>
</div>
</div>
}>
<InfoTravauxClient regionSlug={region} />
</Suspense>
);
}
@@ -0,0 +1,25 @@
import React, { Suspense } from 'react';
import InfoTraficHubClient from '@/components/InfoTraficHubClient';
interface RegionInfoTraficPageProps {
params: Promise<{
region: string;
}>;
}
export default async function RegionInfoTraficPage({ params }: RegionInfoTraficPageProps) {
const { region } = await params;
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de l&apos;info trafic...</p>
</div>
</div>
}>
<InfoTraficHubClient regionSlug={region} />
</Suspense>
);
}
+22
View File
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import FichesHorairesPageClient from '@/components/FichesHorairesPageClient';
export const metadata = {
title: 'Fiches horaires et travaux | TER Ferrovia',
description: 'Consultez les fiches horaires et les affiches travaux de vos lignes',
};
export default function FichesHorairesAliasPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des fiches horaires...</p>
</div>
</div>
}>
<FichesHorairesPageClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
+22
View File
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import GaresPageClient from '@/components/GaresPageClient';
export const metadata = {
title: 'Gares et points d\'arrêts | TER Ferrovia',
description: 'Informations pratiques, services et horaires des gares de votre région',
};
export default function GaresAliasPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de la gare...</p>
</div>
</div>
}>
<GaresPageClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import FichesHorairesPageClient from '@/components/FichesHorairesPageClient';
export const metadata = {
title: 'Fiches horaires et travaux | TER Ferrovia',
description: 'Consultez les fiches horaires et les affiches travaux de vos lignes',
};
export default function FichesHorairesPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des fiches horaires...</p>
</div>
</div>
}>
<FichesHorairesPageClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
+22
View File
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import GaresPageClient from '@/components/GaresPageClient';
export const metadata = {
title: 'Gares et points d\'arrêts | TER Ferrovia',
description: 'Informations pratiques, services et horaires des gares de votre région',
};
export default function GaresPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de la gare...</p>
</div>
</div>
}>
<GaresPageClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import InfoGrevesClient from '@/components/InfoGrevesClient';
export const metadata = {
title: 'Information grève | TER Ferrovia',
description: 'Consultez les prévisions de circulation et informations pratiques en cas de grève',
};
export default function InfoGrevesPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des informations grève...</p>
</div>
</div>
}>
<InfoGrevesClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import InfoTravauxClient from '@/components/InfoTravauxClient';
export const metadata = {
title: 'Information travaux | TER Ferrovia',
description: 'Consultez les dates et impacts des chantiers travaux prévus sur votre réseau régional',
};
export default function InfoTravauxPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement des informations travaux...</p>
</div>
</div>
}>
<InfoTravauxClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
+22
View File
@@ -0,0 +1,22 @@
import React, { Suspense } from 'react';
import InfoTraficHubClient from '@/components/InfoTraficHubClient';
export const metadata = {
title: 'Info trafic, travaux, grève | TER Ferrovia',
description: 'Consultez l\'ensemble des informations trafic, travaux et grèves de votre réseau régional',
};
export default function InfoTraficPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<div className="text-center space-y-3">
<div className="w-10 h-10 border-4 border-[#006837] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="text-sm font-medium text-gray-700">Chargement de l&apos;info trafic...</p>
</div>
</div>
}>
<InfoTraficHubClient regionSlug="bourgogne-franche-comte" />
</Suspense>
);
}
+622
View File
@@ -0,0 +1,622 @@
"use client";
import React, { useState, useEffect, useMemo } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
import { RichText } from '@/components/RegionContent';
interface LigneItem {
id: string;
nom: string;
code?: string;
gare_depart_id?: string;
gare_arrivee_id?: string;
fh_url?: string | null;
region?: string;
actif?: boolean;
}
interface GareItem {
id: string;
nom: string;
}
interface TravauxItem {
id: string | number;
titre: string;
description: string;
date_debut?: string;
date_fin?: string;
jours_travaux?: string[];
heure_debut?: string;
heure_fin?: string;
ligne_id?: string;
ligne_nom?: string;
actif?: boolean;
}
export default function FichesHorairesPageClient({
regionSlug = "bourgogne-franche-comte",
initialLignes = [],
initialGares = [],
initialTravaux = [],
}: {
regionSlug?: string;
initialLignes?: LigneItem[];
initialGares?: GareItem[];
initialTravaux?: TravauxItem[];
}) {
const router = useRouter();
const searchParams = useSearchParams();
const theme = getRegionTheme(regionSlug);
const ligneQuery = searchParams.get('ligne') || '';
const gareQuery = searchParams.get('gare') || '';
// State
const [lignesList, setLignesList] = useState<LigneItem[]>(initialLignes);
const [garesList, setGaresList] = useState<GareItem[]>(initialGares);
const [travauxList, setTravauxList] = useState<TravauxItem[]>(initialTravaux);
const [menuStructure, setMenuStructure] = useState<any>(null);
// Form State
const [selectedLigneId, setSelectedLigneId] = useState<string>('');
const [gareInput, setGareInput] = useState<string>('');
const [showGareSuggestions, setShowGareSuggestions] = useState<boolean>(false);
const [hasSearched, setHasSearched] = useState<boolean>(false);
// Selected Travaux modal for viewing detail
const [activeTravauxModal, setActiveTravauxModal] = useState<TravauxItem | null>(null);
const [activeFhModal, setActiveFhModal] = useState<{ title: string; lineName: string } | null>(null);
// Sync state from query params
useEffect(() => {
if (ligneQuery) {
setSelectedLigneId(ligneQuery);
setGareInput('');
setHasSearched(true);
} else if (gareQuery) {
setGareInput(gareQuery);
setSelectedLigneId('');
setHasSearched(true);
} else {
setHasSearched(false);
}
}, [ligneQuery, gareQuery]);
// Fetch Supabase data
useEffect(() => {
const fetchData = async () => {
try {
// 1. Fetch lignes
const { data: lData } = await supabase
.from('lignes')
.select('*')
.eq('actif', true)
.is('deleted_at', null)
.order('nom', { ascending: true });
if (lData && lData.length > 0) {
setLignesList(lData as LigneItem[]);
}
// 2. Fetch gares
const { data: gData } = await supabase
.from('gares')
.select('id, nom')
.is('deleted_at', null)
.order('nom', { ascending: true });
if (gData && gData.length > 0) {
setGaresList(gData as GareItem[]);
}
// 3. Fetch travaux
const { data: tData } = await supabase
.from('travaux')
.select('*')
.eq('actif', true)
.is('deleted_at', null)
.order('date_debut', { ascending: false });
if (tData && tData.length > 0) {
setTravauxList(tData as TravauxItem[]);
}
// 4. Fetch menu
const { data: mData } = await supabase
.from('menus_regionaux')
.select('*')
.eq('actif', true)
.limit(1);
if (mData && mData.length > 0) {
setMenuStructure(mData[0].structure);
}
} catch (err) {
console.error('Error fetching fiches horaires data:', err);
}
};
fetchData();
}, [regionSlug]);
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
// Autocomplete Suggestions Filter for Gare
const filteredGareSuggestions = useMemo(() => {
if (!gareInput || gareInput.trim().length === 0) return [];
const query = gareInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return garesList.filter(g =>
g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").includes(query)
);
}, [gareInput, garesList]);
// Handle Search Submission
const handleSearch = () => {
if (selectedLigneId) {
setHasSearched(true);
router.push(`${window.location.pathname}?ligne=${encodeURIComponent(selectedLigneId)}`);
} else if (gareInput.trim()) {
setHasSearched(true);
setShowGareSuggestions(false);
router.push(`${window.location.pathname}?gare=${encodeURIComponent(gareInput.trim())}`);
}
};
// Find Active Selected Line
const activeLine = useMemo(() => {
if (!selectedLigneId) return null;
return lignesList.find(l => l.id === selectedLigneId || l.nom.toLowerCase() === selectedLigneId.toLowerCase()) || null;
}, [selectedLigneId, lignesList]);
// Matching Travaux
const matchingTravaux = useMemo(() => {
if (!hasSearched) return [];
if (activeLine) {
const lineNomClean = activeLine.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return travauxList.filter(t => {
if (t.ligne_id && t.ligne_id === activeLine.id) return true;
if (t.ligne_nom) {
const tNomClean = t.ligne_nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return tNomClean.includes(lineNomClean) || lineNomClean.includes(tNomClean);
}
return false;
});
}
if (gareInput.trim()) {
const gareClean = gareInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return travauxList.filter(t => {
const desc = (t.description || '').toLowerCase();
const titre = (t.titre || '').toLowerCase();
const line = (t.ligne_nom || '').toLowerCase();
return desc.includes(gareClean) || titre.includes(gareClean) || line.includes(gareClean);
});
}
return travauxList;
}, [hasSearched, activeLine, gareInput, travauxList]);
// Format line name for official Timetable sheet
const getFicheHoraireTitle = (ligne?: LigneItem | null) => {
if (ligne) {
const nom = ligne.nom.toUpperCase().replace(/\s+/g, '-');
return `${nom} DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026`;
}
if (gareInput.trim()) {
return `LIGNE DESSERVANT ${gareInput.toUpperCase().trim()} DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026`;
}
return "DIJON-SEURRE-BOURG EN BRESSE DU 14 DÉCEMBRE 2025 AU 12 DÉCEMBRE 2026";
};
return (
<div className="min-h-screen bg-white text-gray-900 flex flex-col justify-between font-sans">
<div>
{/* SITE HEADER & MENU */}
<SiteHeader regionSlug={regionSlug} menuStructure={menuStructure} />
<main className="max-w-[1240px] mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Breadcrumb (Images 4 & 5) */}
<div className="flex items-center gap-2 text-[12px] text-gray-600 mb-8">
<Link href={getRegionPath('')} className="hover:underline flex items-center gap-1" style={{ color: theme.primary }}>
<span className="material-icons text-[16px]">home</span>
</Link>
<span className="text-gray-400">›</span>
<span className="font-normal text-gray-700">Fiches horaires</span>
</div>
{/* Centered Top Form Card Container */}
<div className="flex flex-col items-center justify-center text-center max-w-2xl mx-auto pb-6">
{/* Train + Clock Illustration with dynamic theme colors (Images 4 & 5) */}
<div className="relative w-20 h-20 mb-6 flex items-center justify-center" style={{ color: theme.primary }}>
<div
className="w-16 h-16 rounded-full border-2 flex items-center justify-center shadow-2xs"
style={{
backgroundColor: theme.primaryLight,
borderColor: theme.primary
}}
>
{/* Train body */}
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="5" width="16" height="14" rx="3" />
<path d="M4 12h16" />
<circle cx="8" cy="16" r="1.2" fill="currentColor" />
<circle cx="16" cy="16" r="1.2" fill="currentColor" />
<path d="M4 19l-2 2" />
<path d="M20 19l2 2" />
<path d="M8 19h8" />
</svg>
</div>
{/* Overlaid Clock Icon on Top Left */}
<div
className="absolute -top-1 left-0 border-2 rounded-full w-8 h-8 flex items-center justify-center shadow-xs"
style={{
backgroundColor: theme.primaryLight,
borderColor: theme.primary,
color: theme.primary
}}
>
<span className="material-icons text-[18px]">schedule</span>
</div>
</div>
{/* Title & Subtitle */}
<h1 className="text-[26px] sm:text-[28px] font-bold text-gray-900 tracking-tight mb-2.5">
Rechercher une fiche horaire
</h1>
<p className="text-[13.5px] text-gray-600 mb-8 max-w-lg leading-relaxed">
Consulter l&apos;ensemble des fiches horaires, travaux et tous les horaires modifiés de votre ligne.
</p>
{/* Dual Selector: Ligne | ou | Gare (Images 4 & 5) */}
<div className="w-full grid grid-cols-1 md:grid-cols-[1fr,auto,1fr] items-center gap-3 bg-white">
{/* Ligne Selector */}
<div className="text-left w-full">
<label className="block text-[13px] font-medium text-gray-700 mb-1.5">Ligne</label>
<div className="relative bg-[#f4f5f7] rounded-lg border border-gray-200 focus-within:border-gray-400 flex items-center shadow-2xs">
<span className="material-icons text-[18px] pl-3.5 pr-2 pointer-events-none" style={{ color: theme.primary }}>
alt_route
</span>
<select
value={selectedLigneId}
onChange={(e) => {
setSelectedLigneId(e.target.value);
if (e.target.value) setGareInput('');
}}
className="w-full bg-transparent py-3 pr-8 text-[13.5px] text-gray-800 outline-none appearance-none cursor-pointer"
>
<option value="">Ligne</option>
{lignesList.map((l) => (
<option key={l.id} value={l.id}>
{l.nom.toUpperCase()}
</option>
))}
</select>
<span className="material-icons text-[18px] absolute right-3 pointer-events-none" style={{ color: theme.primary }}>
expand_more
</span>
</div>
</div>
{/* Separator: | ou | */}
<div className="hidden md:flex flex-col items-center justify-center px-2 pt-6 text-gray-400 text-[12px]">
<span>ou</span>
</div>
{/* Gare Selector */}
<div className="text-left w-full relative">
<label className="block text-[13px] font-medium text-gray-700 mb-1.5">Gare</label>
<div className="relative bg-[#f4f5f7] rounded-lg border border-gray-200 focus-within:border-gray-400 flex items-center shadow-2xs">
<span className="material-icons text-[18px] pl-3.5 pr-2 pointer-events-none" style={{ color: theme.primary }}>
place
</span>
<input
type="text"
value={gareInput}
onChange={(e) => {
setGareInput(e.target.value);
setShowGareSuggestions(true);
if (e.target.value) setSelectedLigneId('');
}}
onFocus={() => setShowGareSuggestions(true)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSearch();
}}
placeholder="Gare"
className="w-full bg-transparent py-3 pr-3 text-[13.5px] text-gray-800 placeholder-gray-500 outline-none"
/>
{gareInput && (
<button
type="button"
onClick={() => {
setGareInput('');
setShowGareSuggestions(false);
}}
className="p-2 text-gray-400 hover:text-gray-600 cursor-pointer"
>
<span className="material-icons text-[18px]">close</span>
</button>
)}
</div>
{/* Suggestions Dropdown for Gare */}
{showGareSuggestions && filteredGareSuggestions.length > 0 && (
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-gray-200 rounded-lg shadow-xl max-h-56 overflow-y-auto z-50 text-left divide-y divide-gray-100 animate-fadeIn">
{filteredGareSuggestions.map((g) => (
<div
key={g.id}
onClick={() => {
setGareInput(g.nom);
setShowGareSuggestions(false);
setSelectedLigneId('');
}}
className="px-4 py-2.5 hover:bg-gray-50 cursor-pointer text-[13px] text-gray-800 transition-colors"
style={{ color: theme.primary }}
>
{g.nom}
</div>
))}
</div>
)}
</div>
</div>
{/* Rechercher Button with Dynamic Theme Color */}
<button
type="button"
onClick={handleSearch}
className={`mt-6 text-[13.5px] font-semibold px-9 py-2.5 rounded-md transition-all shadow-xs cursor-pointer min-w-[140px] text-white`}
style={{
backgroundColor: hasSearched || selectedLigneId || gareInput ? theme.primary : '#383d41'
}}
>
Rechercher
</button>
</div>
{/* ─── STEP 2: RESULTS SECTION (Image 5) ─── */}
{hasSearched && (
<div className="mt-8 pt-6 border-t border-gray-200 animate-fadeIn">
{/* Section Header: En cours with theme underline */}
<div className="mb-6">
<h2
className="text-[17px] font-bold text-gray-900 pb-2 border-b-2 inline-block"
style={{ borderColor: theme.secondary || theme.primary }}
>
En cours
</h2>
</div>
{/* List of Results (Travaux notices + Official Timetable Sheets) */}
<div className="space-y-4 max-w-3xl">
{/* 1. Affiches Travaux Notices (Orange circular icons) */}
{matchingTravaux.length > 0 ? (
matchingTravaux.map((trav) => (
<div
key={trav.id}
onClick={() => setActiveTravauxModal(trav)}
className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1"
>
{/* Orange Round Icon */}
<span className="w-6 h-6 rounded-full bg-[#e65100] text-white flex items-center justify-center shrink-0 shadow-2xs">
<span className="material-icons text-[14px]">construction</span>
</span>
{/* Text */}
<span className="font-semibold underline underline-offset-2">
Affiches Travaux {trav.ligne_nom || activeLine?.nom || 'Ligne'} {trav.date_debut && trav.date_fin ? `du ${new Date(trav.date_debut).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' })} au ${new Date(trav.date_fin).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}` : ''}
</span>
<span className="text-gray-400 font-bold">›</span>
</div>
))
) : (
<div
onClick={() => setActiveTravauxModal({
id: 'default-travaux',
titre: `Travaux d'entretiens des voies - ${activeLine?.nom || gareInput || 'Ligne'}`,
description: `<p><strong>Actuellement et jusqu'au 4 octobre du lundi au vendredi</strong><br/>Suppression de certains trains en milieu de journée.<br/>Des autocars de remplacement sont mis en place entre les principales gares.</p>`,
ligne_nom: activeLine?.nom || gareInput || 'Ligne'
})}
className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1"
>
<span className="w-6 h-6 rounded-full bg-[#e65100] text-white flex items-center justify-center shrink-0 shadow-2xs">
<span className="material-icons text-[14px]">construction</span>
</span>
<span className="font-semibold underline underline-offset-2">
Affiches Travaux {activeLine?.nom || gareInput || 'Ligne Régionale'} du 1 septembre au 4 octobre 2026
</span>
<span className="text-gray-400 font-bold">›</span>
</div>
)}
{/* 2. Official Timetable PDF / Viewer Sheet with Dynamic Theme Color */}
<div
onClick={() => setActiveFhModal({
title: getFicheHoraireTitle(activeLine),
lineName: activeLine?.nom || gareInput || 'Ligne Régionale'
})}
className="flex items-center gap-3 text-[13.5px] text-gray-900 hover:opacity-80 cursor-pointer group transition-colors py-1"
>
{/* Round Icon with dynamic theme color */}
<span
className="w-6 h-6 rounded-full text-white flex items-center justify-center shrink-0 shadow-2xs"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[14px]">train</span>
</span>
{/* Text */}
<span className="font-bold underline underline-offset-2 uppercase">
{getFicheHoraireTitle(activeLine)}
</span>
<span className="text-gray-400 font-bold">›</span>
</div>
</div>
</div>
)}
</main>
</div>
{/* MODAL TRAVAUX DETAILS */}
{activeTravauxModal && (
<div
className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4 animate-fadeIn"
onClick={() => setActiveTravauxModal(null)}
>
<div
className="bg-white rounded-xl shadow-2xl max-w-lg w-full max-h-[85vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-4 bg-[#fff3e0] border-b border-[#ffe0b2] flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-icons text-[#e65100]">construction</span>
<span className="font-bold text-[15px] text-[#e65100]">Affiche Travaux</span>
</div>
<button
type="button"
onClick={() => setActiveTravauxModal(null)}
className="text-gray-400 hover:text-black p-1 cursor-pointer"
>
<span className="material-icons text-[20px]">close</span>
</button>
</div>
{/* Body */}
<div className="p-5 overflow-y-auto space-y-3">
<h3 className="font-bold text-[16px] text-gray-900">
{activeTravauxModal.titre}
</h3>
{activeTravauxModal.ligne_nom && (
<div
className="text-[12.5px] font-semibold px-2.5 py-1 rounded inline-block"
style={{
backgroundColor: theme.primaryLight,
color: theme.primary
}}
>
Ligne : {activeTravauxModal.ligne_nom}
</div>
)}
<div className="text-[13px] text-gray-700 leading-relaxed border-t border-gray-100 pt-3">
<RichText content={activeTravauxModal.description || ''} />
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2">
<button
type="button"
onClick={() => {
window.print();
}}
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 text-[12.5px] font-semibold hover:bg-white flex items-center gap-1.5"
>
<span className="material-icons text-[16px]">print</span>
<span>Imprimer l&apos;affiche</span>
</button>
<button
type="button"
onClick={() => setActiveTravauxModal(null)}
className="px-5 py-2 rounded-lg text-white text-[12.5px] font-semibold hover:opacity-90"
style={{ backgroundColor: theme.primary }}
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* MODAL FICHE HORAIRE PDF / VIEWER */}
{activeFhModal && (
<div
className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4 animate-fadeIn"
onClick={() => setActiveFhModal(null)}
>
<div
className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div
className="p-4 border-b flex items-center justify-between"
style={{
backgroundColor: theme.primaryLight,
borderColor: theme.secondary || theme.primary
}}
>
<div className="flex items-center gap-2">
<span className="material-icons" style={{ color: theme.primary }}>description</span>
<span className="font-bold text-[15px]" style={{ color: theme.primary }}>Fiche Horaire Officielle</span>
</div>
<button
type="button"
onClick={() => setActiveFhModal(null)}
className="text-gray-400 hover:text-black p-1 cursor-pointer"
>
<span className="material-icons text-[20px]">close</span>
</button>
</div>
{/* Body */}
<div className="p-6 overflow-y-auto space-y-4 text-center">
<div
className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-2"
style={{
backgroundColor: theme.primaryLight,
color: theme.primary
}}
>
<span className="material-icons text-[32px]">picture_as_pdf</span>
</div>
<h3 className="font-extrabold text-[17px] text-gray-900 uppercase">
{activeFhModal.title}
</h3>
<p className="text-[13px] text-gray-600 max-w-md mx-auto">
Téléchargez ou imprimez la grille horaire complète valable pour la période 2025 - 2026.
</p>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 text-left text-[12.5px] space-y-1.5">
<p><span className="font-bold text-gray-800">Réseau :</span> TER {theme.regionName} ({theme.operatorLabel})</p>
<p><span className="font-bold text-gray-800">Ligne :</span> {activeFhModal.lineName}</p>
<p><span className="font-bold text-gray-800">Format :</span> Document PDF Officiel</p>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2">
<button
type="button"
onClick={() => {
window.print();
}}
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 text-[12.5px] font-semibold hover:bg-white flex items-center gap-1.5"
>
<span className="material-icons text-[16px]">print</span>
<span>Imprimer</span>
</button>
<button
type="button"
onClick={() => {
alert(`Téléchargement de la fiche horaire : ${activeFhModal.title}`);
setActiveFhModal(null);
}}
className="px-5 py-2 rounded-lg text-white text-[12.5px] font-semibold hover:opacity-90 flex items-center gap-1.5"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[16px]">download</span>
<span>Télécharger le PDF</span>
</button>
</div>
</div>
</div>
)}
{/* REUSABLE SITE FOOTER (Image 1) */}
<SiteFooter regionSlug={regionSlug} />
</div>
);
}
+908
View File
@@ -0,0 +1,908 @@
"use client";
import React, { useState, useEffect, useMemo } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
import TimetableSearchForm from '@/components/TimetableSearchForm';
import StationDeparturesBoard from '@/components/StationDeparturesBoard';
export interface LigneTransportInfo {
nom?: string;
terminus_a?: string;
terminus_b?: string;
}
export interface GareInformations {
adresse?: string;
parking?: boolean;
toilettes?: boolean;
espace_vtc?: boolean;
lignes_bus?: (string | LigneTransportInfo)[];
photomaton?: boolean;
espace_taxis?: boolean;
lignes_metro?: (string | LigneTransportInfo)[];
wifi_gratuit?: boolean;
depose_minute?: boolean;
horaires_gare?: string;
parking_velos?: boolean;
defibrillateur?: boolean;
espace_attente?: boolean;
lignes_tramway?: (string | LigneTransportInfo)[];
objets_trouves?: boolean;
distributeur_ter?: boolean;
distributeur_tgv?: boolean;
horaires_guichet?: string;
location_voiture?: boolean;
service_assistance?: boolean;
velo_libre_service?: boolean;
galeries_marchandes?: boolean;
lignes_car_regional?: (string | LigneTransportInfo)[];
}
export function formatLineDetail(line: string | LigneTransportInfo): string {
if (!line) return '';
if (typeof line === 'string') return line;
if (typeof line === 'object') {
const nom = line.nom || '';
if (line.terminus_a && line.terminus_b) {
return `Ligne ${nom} (${line.terminus_a.trim()} - ${line.terminus_b.trim()})`;
}
if (line.terminus_a || line.terminus_b) {
return `Ligne ${nom} (${(line.terminus_a || line.terminus_b || '').trim()})`;
}
return nom ? `Ligne ${nom}` : '';
}
return String(line);
}
export interface GareItem {
id: string;
nom: string;
type?: string;
services?: any;
informations?: GareInformations;
}
interface HoraireGareDesservie {
gare_id?: string;
gare_nom: string;
quai?: string | null;
heure_depart?: string | null;
heure_arrivee?: string | null;
ordre?: number;
is_depart?: boolean;
is_arrivee?: boolean;
}
interface HoraireItem {
id: string | number;
numero_train?: string;
type_train?: string;
ligne_nom?: string;
heure_depart?: string;
heure_arrivee?: string;
gare_depart_nom?: string;
gare_arrivee_nom?: string;
gares_desservies?: HoraireGareDesservie[] | string;
jours_personnalises?: any[];
actif?: boolean;
}
interface ResolvedDeparture {
id: string | number;
scheduledTime: string;
destName: string;
trainLabel: string;
trainNumber: string;
track: string;
hasDelay: boolean;
delayText?: string;
hasInfo: boolean;
infoText?: string;
}
export default function GaresPageClient({
regionSlug = "bourgogne-franche-comte",
initialGares = [],
initialSelectedGare = null,
}: {
regionSlug?: string;
initialGares?: GareItem[];
initialSelectedGare?: string | null;
}) {
const router = useRouter();
const searchParams = useSearchParams();
const theme = getRegionTheme(regionSlug);
// Read ?gare= from query parameters or prop
const gareQuery = searchParams.get('gare') || initialSelectedGare || '';
// State
const [garesList, setGaresList] = useState<GareItem[]>(initialGares);
const [searchInput, setSearchInput] = useState<string>(gareQuery);
const [selectedGareName, setSelectedGareName] = useState<string>(gareQuery);
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
const [showAllStationsModal, setShowAllStationsModal] = useState<boolean>(false);
const [menuStructure, setMenuStructure] = useState<any>(null);
// Sync selected gare when query changes
useEffect(() => {
if (gareQuery) {
setSelectedGareName(gareQuery);
setSearchInput(gareQuery);
} else {
setSelectedGareName('');
setSearchInput('');
}
}, [gareQuery]);
// Fetch Gares & Horaires from Supabase
useEffect(() => {
const fetchData = async () => {
try {
// 1. Fetch gares
const { data: gData } = await supabase
.from('gares')
.select('id, nom, type, services, informations')
.is('deleted_at', null)
.order('nom', { ascending: true });
if (gData && gData.length > 0) {
setGaresList(gData as GareItem[]);
}
// 2. Fetch menu
const { data: mData } = await supabase
.from('menus_regionaux')
.select('*')
.eq('actif', true)
.limit(1);
if (mData && mData.length > 0) {
setMenuStructure(mData[0].structure);
}
} catch (err) {
console.error('Error fetching data for Gares page:', err);
}
};
fetchData();
}, [regionSlug]);
// Selected Gare Object from DB
const currentGareObj = useMemo(() => {
if (!selectedGareName) return null;
const cleanName = selectedGareName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return garesList.find(g =>
g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim() === cleanName ||
g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().includes(cleanName) ||
cleanName.includes(g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim())
) || null;
}, [selectedGareName, garesList]);
// Dynamic values extracted from informations column
const info: GareInformations = currentGareObj?.informations || {};
// Autocomplete Suggestions Filter
const filteredSuggestions = useMemo(() => {
if (!searchInput || searchInput.trim().length === 0) return [];
const query = searchInput.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return garesList.filter(g =>
g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").includes(query)
);
}, [searchInput, garesList]);
// Handle Search Submission
const handleSearch = (targetName?: string) => {
const finalName = (targetName !== undefined ? targetName : searchInput).trim();
if (!finalName) return;
setSelectedGareName(finalName);
setShowSuggestions(false);
const params = new URLSearchParams(window.location.search);
params.set('gare', finalName);
router.push(`${window.location.pathname}?${params.toString()}`);
};
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
return (
<div className="min-h-screen bg-white text-gray-900 flex flex-col justify-between font-sans">
<div>
{/* SITE HEADER & MENU */}
<SiteHeader regionSlug={regionSlug} menuStructure={menuStructure} />
{/* ─── STEP 1: RECHERCHE DE GARE (Image 2) ─── */}
{!selectedGareName ? (
<main className="max-w-[1240px] mx-auto px-4 sm:px-6 py-6 sm:py-10">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-[12px] text-gray-600 mb-8 sm:mb-12">
<Link href={getRegionPath('')} className="hover:underline flex items-center gap-1" style={{ color: theme.primary }}>
<span className="material-icons text-[16px]">home</span>
</Link>
<span className="text-gray-400">›</span>
<span className="font-normal text-gray-700">Rechercher une gare</span>
</div>
{/* Centered Search Card Container */}
<div className="flex flex-col items-center justify-center text-center max-w-xl mx-auto pt-4 pb-12">
{/* Train Illustration in Theme Badge */}
<div
className="w-20 h-20 mb-6 flex items-center justify-center rounded-full border-2 shadow-2xs transition-colors"
style={{
backgroundColor: theme.primaryLight,
borderColor: theme.primary,
color: theme.primary
}}
>
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="3" width="16" height="15" rx="3" />
<path d="M4 11h16" />
<path d="M12 3v8" />
<circle cx="8" cy="15" r="1.2" fill="currentColor" />
<circle cx="16" cy="15" r="1.2" fill="currentColor" />
<path d="M4 18l-2 3" />
<path d="M20 18l2 3" />
<path d="M8 18h8" />
</svg>
</div>
{/* Title & Subtitle */}
<h1 className="text-[26px] sm:text-[28px] font-bold text-gray-900 tracking-tight mb-2.5">
Rechercher une gare
</h1>
<p className="text-[13.5px] text-gray-600 mb-8 max-w-md leading-relaxed">
Informations pratiques et tableaux des départs et arrivées de plus de 5000 gares
</p>
{/* Station Search Input with Autocomplete */}
<div className="w-full relative max-w-md">
<div className="relative flex items-center bg-[#f4f5f7] rounded-lg border border-gray-200 focus-within:border-gray-400 transition-all shadow-2xs">
<span className="material-icons text-[20px] pl-3.5 pr-2 pointer-events-none" style={{ color: theme.primary }}>
place
</span>
<input
type="text"
value={searchInput}
onChange={(e) => {
setSearchInput(e.target.value);
setShowSuggestions(true);
}}
onFocus={() => setShowSuggestions(true)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSearch();
}}
placeholder="Gare"
className="w-full bg-transparent py-3 pr-4 text-[14px] text-gray-900 placeholder-gray-500 outline-none"
/>
{searchInput && (
<button
type="button"
onClick={() => {
setSearchInput('');
setShowSuggestions(false);
}}
className="p-2 text-gray-400 hover:text-gray-600 cursor-pointer"
>
<span className="material-icons text-[18px]">close</span>
</button>
)}
</div>
{/* Suggestions Dropdown */}
{showSuggestions && filteredSuggestions.length > 0 && (
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-gray-200 rounded-lg shadow-xl max-h-60 overflow-y-auto z-50 text-left divide-y divide-gray-100 animate-fadeIn">
{filteredSuggestions.map((g) => (
<div
key={g.id}
onClick={() => {
setSearchInput(g.nom);
handleSearch(g.nom);
}}
className="px-4 py-2.5 hover:bg-gray-50 cursor-pointer text-[13.5px] text-gray-800 flex items-center justify-between transition-colors"
>
<span className="font-medium">{g.nom}</span>
<span className="text-[11px] text-gray-400 capitalize">{g.type || 'Gare'}</span>
</div>
))}
</div>
)}
</div>
{/* Submit Button (Pill button) */}
<button
type="button"
onClick={() => handleSearch()}
className="mt-6 bg-[#383d41] hover:bg-[#2a2d30] text-white text-[13.5px] font-semibold px-8 py-2.5 rounded-md transition-all shadow-xs cursor-pointer min-w-[140px]"
>
Rechercher
</button>
{/* Link: Voir la liste des gares et points d'arrêts */}
<button
type="button"
onClick={() => setShowAllStationsModal(true)}
className="mt-6 text-[13px] text-gray-700 hover:underline font-normal inline-flex items-center gap-1 cursor-pointer"
style={{ color: theme.primary }}
>
<span>Voir la liste des gares et points d&apos;arrêts</span>
<span className="font-bold">›</span>
</button>
</div>
</main>
) : (
/* ─── STEP 2: DÉTAIL DE LA GARE (Image 3 - DYNAMIQUE BDD) ─── */
<main className="w-full pb-12">
<div className="max-w-[1240px] mx-auto px-4 sm:px-6 pt-4 pb-4">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-[12px] text-gray-600 mb-4">
<Link href={getRegionPath('')} className="hover:underline flex items-center gap-1" style={{ color: theme.primary }}>
<span className="material-icons text-[16px]">home</span>
</Link>
<span className="text-gray-400">›</span>
<Link href={getRegionPath('/se-deplacer/gares')} className="hover:underline text-gray-700">
Rechercher une gare
</Link>
<span className="text-gray-400">›</span>
<span className="font-medium text-gray-900 capitalize">{selectedGareName}</span>
</div>
{/* Theme Header Banner: GARE DE [NOM] */}
<div
className="w-full text-white text-center py-4 px-6 rounded-md shadow-xs mb-8 transition-colors"
style={{ backgroundColor: theme.bannerBg || theme.secondary || theme.primary }}
>
<h1 className="text-[20px] sm:text-[22px] font-extrabold tracking-wide uppercase">
GARE DE {selectedGareName}
</h1>
</div>
{/* 1. Practical Info Block (Dynamique depuis informations) */}
<div className="space-y-4 mb-8 text-[13.5px] text-gray-800">
{/* Adresse */}
<div className="flex items-start gap-3">
<span className="material-icons text-[20px] mt-0.5" style={{ color: theme.primary }}>place</span>
<div>
<span className="font-bold block text-gray-900">Adresse</span>
<p className="text-gray-600 leading-snug whitespace-pre-line">
{info.adresse && info.adresse.trim().length > 0
? info.adresse
: (selectedGareName.toLowerCase().includes('dijon') ? "21 cour de la gare\n21000 Dijon" : `Parvis de la Gare\n${selectedGareName}`)}
</p>
</div>
</div>
{/* Horaires d'ouverture */}
<div className="flex items-start gap-3">
<span className="material-icons text-[20px] mt-0.5" style={{ color: theme.primary }}>schedule</span>
<div className="space-y-2">
<span className="font-bold block text-gray-900">Horaires d&apos;ouverture</span>
<div>
<span className="font-semibold text-gray-800 text-[13px] block">Gare</span>
<p className="text-gray-600 text-[12.5px] leading-relaxed whitespace-pre-line">
{info.horaires_gare && info.horaires_gare.trim().length > 0
? info.horaires_gare
: "Lundi à vendredi : de 4h30 à 23h15\nDimanche et jours fériés : de 5h45 à 23h15"}
</p>
</div>
<div className="pt-1">
<span className="font-semibold text-gray-800 text-[13px] block">Guichet</span>
<div className="space-y-1.5 text-[12.5px] text-gray-600 pt-1">
{info.horaires_guichet && info.horaires_guichet.trim().length > 0 ? (
<p className="whitespace-pre-line">{info.horaires_guichet}</p>
) : (
<>
<div>
<span className="font-bold text-gray-700 block uppercase text-[11px] tracking-wider">
ESPACE DE VENTE TRAIN {theme.operatorLabel} :
</span>
<p>Lundi à vendredi : de 7h00 à 19h00</p>
<p>Samedi : de 8h00 à 19h00</p>
<p>Dimanche et jours fériés : de 10h00 à 19h00</p>
</div>
<div className="pt-1">
<span className="font-bold text-gray-700 block uppercase text-[11px] tracking-wider">
ESPACE DE VENTE GRANDES LIGNES :
</span>
<p>Lundi à samedi : de 9h00 à 18h00</p>
<p>Dimanche et jours fériés : fermé</p>
</div>
</>
)}
</div>
</div>
</div>
</div>
{/* Distributeurs TER & TGV */}
{(info.distributeur_ter !== false) && (
<div className="flex items-center gap-3 pt-1">
<span className="material-icons text-[20px]" style={{ color: theme.primary }}>confirmation_number</span>
<span className="font-semibold text-gray-800">Distributeur de titre de transport TER</span>
</div>
)}
{info.distributeur_tgv && (
<div className="flex items-center gap-3">
<span className="material-icons text-[20px]" style={{ color: theme.primary }}>confirmation_number</span>
<span className="font-semibold text-gray-800">Distributeur de billets Grandes Lignes</span>
</div>
)}
{/* Achetez vos billets sur mobile */}
<div className="flex items-center gap-3">
<span className="material-icons text-[20px]" style={{ color: theme.primary }}>smartphone</span>
<div>
<span className="font-semibold text-gray-800 block">Achetez aussi vos billets sur votre mobile</span>
<Link
href={getRegionPath(`/horaires?depart=${encodeURIComponent(selectedGareName)}`)}
className="hover:underline text-[12.5px] font-medium inline-flex items-center gap-0.5"
style={{ color: theme.primary }}
>
<span>Rechercher un trajet</span>
<span>›</span>
</Link>
</div>
</div>
</div>
{/* 2. Blue Info Banner: "Bon à savoir" */}
<div className="w-full bg-[#e8f3fb] border-l-4 border-[#0288d1] p-4 rounded-r-md flex items-start gap-3 mb-10 text-[13px] text-gray-800">
<span className="material-icons text-[#0288d1] text-[22px] shrink-0 mt-0.5">info</span>
<div>
<span className="font-bold text-gray-900 block mb-0.5">Bon à savoir</span>
<p className="text-gray-700 leading-relaxed">
Informations et vente de billets : le site {theme.operatorLabel} (
<a href="https://viamobigo.fr" target="_blank" rel="noopener noreferrer" className="underline font-medium" style={{ color: theme.primary }}>
service-client.{regionSlug}.fr
</a>
) est à votre disposition ou par téléphone au <span className="font-semibold">03 80 11 29 29</span> du lundi au samedi de 7h à 20h.
</p>
</div>
</div>
{/* 3. Section Accessibilité */}
<div className="mb-10">
<h2 className="text-[17px] font-bold text-gray-900 mb-4 pb-1.5 border-b border-gray-200">
Accessibilité
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Card 1 */}
<div className="flex items-start gap-3.5 p-4 bg-[#fcfdfa] border border-gray-200 rounded-lg">
<span className="material-icons text-[28px] shrink-0" style={{ color: theme.primary }}>accessible_forward</span>
<div className="text-[12.5px] text-gray-700 space-y-1.5">
<span className="font-bold text-gray-900 block text-[13.5px]">
Les services d&apos;assistance et équipements dans cette gare
</span>
<p className="leading-relaxed">
{info.service_assistance !== false
? "Retrouvez pour cette gare les informations d'accessibilité ainsi que les équipements disponibles (ascenseur, élévateur, bande d'éveil...) sur Gares & Connexions ou dans le document ci-dessous."
: "Gare équipée de dispositifs d'accès adaptés. Contactez le service d'assistance avant votre départ."}
</p>
<a href="#" className="hover:underline font-semibold block pt-1" style={{ color: theme.primary }}>
Consulter les équipements et accessibilité ›
</a>
</div>
</div>
{/* Card 2 */}
<div className="flex items-start gap-3.5 p-4 bg-[#fcfdfa] border border-gray-200 rounded-lg">
<span className="material-icons text-[28px] shrink-0" style={{ color: theme.primary }}>info</span>
<div className="text-[12.5px] text-gray-700 space-y-1.5">
<span className="font-bold text-gray-900 block text-[13.5px]">
Informations générales sur les services d&apos;assistance en gare et les équipements à bord des trains
</span>
<p className="leading-relaxed">
Découvrez comment bénéficier d&apos;une prise en charge personnalisée pour vos trajets régionaux.
</p>
<a href="#" className="hover:underline font-semibold block pt-1" style={{ color: theme.primary }}>
Accéder à la rubrique ›
</a>
</div>
</div>
</div>
</div>
{/* 4. Section Services (Dynamique selon informations) */}
<div className="mb-12">
<h2 className="text-[17px] font-bold text-gray-900 mb-6 pb-1.5 border-b border-gray-200">
Services
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6 text-[13px] text-gray-700">
{/* Détente / Espace attente */}
{(info.espace_attente !== false) && (
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>weekend</span>
<div>
<span className="font-bold text-gray-900 block">Détente</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Salon d&apos;attente</li>
<li>Espace repos</li>
</ul>
</div>
</div>
)}
{/* Boutique / Galeries */}
{(info.galeries_marchandes !== false) && (
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>storefront</span>
<div>
<span className="font-bold text-gray-900 block">Boutique</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Relay / vente journaux</li>
<li>Vente de pain, légumes, fromages, miel... 2 jours/semaine sur le parvis de la gare (En savoir plus &gt;)</li>
</ul>
</div>
</div>
)}
{/* Restauration */}
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>local_cafe</span>
<div>
<span className="font-bold text-gray-900 block">Restauration</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Distributeurs de boissons / friandises</li>
<li>Restauration rapide</li>
<li>Vente à emporter</li>
</ul>
</div>
</div>
{/* Objets trouvés */}
{(info.objets_trouves !== false) && (
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>find_in_page</span>
<div>
<span className="font-bold text-gray-900 block">Objets trouvés</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Perdu et trouvé (au guichet de la gare)</li>
</ul>
</div>
</div>
)}
{/* Consigne colis */}
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>inventory_2</span>
<div>
<span className="font-bold text-gray-900 block">Consigne colis</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Pickup Station</li>
</ul>
</div>
</div>
{/* Toilettes */}
{(info.toilettes !== false) && (
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>wc</span>
<div>
<span className="font-bold text-gray-900 block">Toilettes</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
<li>Toilettes payantes et accessibles pour les personnes à mobilité réduite</li>
</ul>
</div>
</div>
)}
{/* Autres services (Photomaton, Defibrillateur, Wifi...) */}
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>widgets</span>
<div>
<span className="font-bold text-gray-900 block">Autres services</span>
<ul className="list-disc pl-4 text-gray-600 mt-1 space-y-0.5">
{(info.photomaton !== false) && <li>Cabine photographique</li>}
{(info.wifi_gratuit !== false) && <li>Wifi gratuit en gare</li>}
{(info.defibrillateur !== false) && <li>Défibrillateur cardiaque disponible</li>}
<li>Photocopieur</li>
<li>Boîte aux lettres à l&apos;extérieur, sur le parvis de la gare</li>
</ul>
</div>
</div>
{/* Distributeur de billets */}
<div className="flex items-start gap-3">
<span className="material-icons text-[22px] shrink-0" style={{ color: theme.primary }}>credit_card</span>
<div>
<span className="font-bold text-gray-900 block">Distributeur de billets de banque</span>
<p className="text-gray-600 mt-1">Disponible dans le hall principal.</p>
</div>
</div>
</div>
</div>
{/* 5. Section Intermodalité & Transports en commun (Dynamique) */}
<div className="mb-12">
<h2 className="text-[17px] font-bold text-gray-900 mb-6 text-center">
Transport en commun
</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
{/* Bus Card */}
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">directions_bus</span>
<span className="font-bold text-[14px]">Bus</span>
</div>
<div className="p-4 text-[12.5px] text-gray-700 space-y-1.5 text-center">
<p className="font-semibold text-gray-900">Réseau Urbain :</p>
{Array.isArray(info.lignes_bus) && info.lignes_bus.length > 0 ? (
<div className="space-y-1 text-left max-h-36 overflow-y-auto pr-1">
{info.lignes_bus.map((b, idx) => (
<p key={idx} className="leading-tight">• {formatLineDetail(b)}</p>
))}
</div>
) : (
<p>Lignes urbaines desservant la gare</p>
)}
<p className="text-gray-500 text-[11.5px] pt-1">Tél : 03 80 11 29 29</p>
</div>
</div>
{/* Tramway Card (if exists or default) */}
{(Array.isArray(info.lignes_tramway) && info.lignes_tramway.length > 0) || selectedGareName.toLowerCase().includes('dijon') ? (
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">tram</span>
<span className="font-bold text-[14px]">Tramway</span>
</div>
<div className="p-4 text-[12.5px] text-gray-700 space-y-1.5 text-center">
<p className="font-semibold text-gray-900">Réseau Tram :</p>
{Array.isArray(info.lignes_tramway) && info.lignes_tramway.length > 0 ? (
<div className="space-y-1 text-left max-h-36 overflow-y-auto pr-1">
{info.lignes_tramway.map((t, idx) => (
<p key={idx} className="leading-tight">• {formatLineDetail(t)}</p>
))}
</div>
) : (
<div className="space-y-1 text-left">
<p className="leading-tight">• Ligne T1 (Gare SNCF - Dijon Valmy / Quetigny)</p>
<p className="leading-tight">• Ligne T2 (Chenôve / Valmy Zenon - Grand Sud)</p>
</div>
)}
<p className="pt-1 text-gray-500 text-[11.5px]">Tél : 03 80 11 29 29</p>
</div>
</div>
) : null}
{/* Car Card */}
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">airport_shuttle</span>
<span className="font-bold text-[14px]">Car</span>
</div>
<div className="p-4 text-[12.5px] text-gray-700 space-y-1.5 text-center">
<p className="font-semibold text-gray-900">Réseau {theme.operatorLabel}</p>
{Array.isArray(info.lignes_car_regional) && info.lignes_car_regional.length > 0 ? (
<div className="space-y-1 text-left max-h-36 overflow-y-auto pr-1">
{info.lignes_car_regional.map((c, idx) => (
<p key={idx} className="leading-tight">• {formatLineDetail(c)}</p>
))}
</div>
) : (
<p>Autocars régionaux réguliers</p>
)}
<p className="text-[11px] text-gray-500 pt-1">Renseignements sur le portail régional</p>
</div>
</div>
</div>
{/* Vélo Card */}
{(info.parking_velos !== false || info.velo_libre_service !== false) && (
<>
<h3 className="text-[16px] font-bold text-gray-900 mb-4 text-center">Vélo</h3>
<div className="max-w-xs mx-auto mb-6">
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">pedal_bike</span>
<span className="font-bold text-[14px]">Vélo</span>
</div>
<div className="p-4 text-[12.5px] text-gray-700 space-y-1 text-center">
<p>• Abris vélos sécurisés</p>
<p>• Location vélos en libre-service</p>
</div>
</div>
</div>
</>
)}
{/* Voiture & Taxi Header & Cards */}
<h3 className="text-[16px] font-bold text-gray-900 mb-4 text-center">Voiture & Taxi</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
{/* Voiture Card */}
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">directions_car</span>
<span className="font-bold text-[14px]">Voiture</span>
</div>
<div className="p-4 text-[12px] text-gray-700 space-y-1">
{(info.depose_minute !== false) && <p>• Dépose-minute devant la gare</p>}
{(info.parking !== false) && <p>• Parking courte et longue durée</p>}
<p>• Bornes de recharge électrique</p>
{(info.location_voiture !== false) && <p>• Loueurs : AVIS, Enterprise, Europcar, Hertz, Sixt</p>}
</div>
</div>
{/* Taxi Card */}
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white shadow-2xs">
<div
className="py-3 text-center flex flex-col items-center justify-center text-white"
style={{ backgroundColor: theme.secondary || theme.bannerBg || theme.primary }}
>
<span className="material-icons text-[24px]">local_taxi</span>
<span className="font-bold text-[14px]">Taxi</span>
</div>
<div className="p-4 text-[12.5px] text-gray-700 space-y-1 text-center">
<p>• Station Taxi devant la gare</p>
{(info.espace_vtc !== false) && <p>• Espace de prise en charge VTC</p>}
<p className="text-gray-500 text-[11.5px]">Tél Taxi : 03 80 41 41 12</p>
</div>
</div>
</div>
</div>
</div>
{/* 6. GREY WIDGETS SECTION: TIMETABLE FORM + LIVE DEPARTURES (SIDE-BY-SIDE) */}
<div className="w-full bg-[#f4f5f7] py-10 border-y border-gray-200">
<div className="max-w-[1240px] mx-auto px-4 sm:px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
{/* Left: Timetable Search Form Widget */}
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
<h3 className="text-[15px] font-bold text-gray-900 mb-4 flex items-center gap-2">
<span className="material-icons text-[18px]" style={{ color: theme.primary }}>search</span>
<span>Rechercher un horaire depuis {selectedGareName}</span>
</h3>
<TimetableSearchForm
regionSlug={regionSlug}
initialDeparture={selectedGareName}
/>
</div>
{/* Right: Prochains départs en gare de [Gare] Reusable Component */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<StationDeparturesBoard
regionSlug={regionSlug}
initialStation={selectedGareName}
showStationSelector={false}
/>
</div>
</div>
</div>
</div>
{/* 7. SECTION HORAIRES ET TRAVAUX */}
<div className="max-w-[1240px] mx-auto px-4 sm:px-6 py-8 text-center">
<h3 className="text-[15px] font-bold text-gray-900 uppercase tracking-wide mb-1">
HORAIRES ET TRAVAUX
</h3>
<Link
href={getRegionPath(`/se-deplacer/fiches-horaires?gare=${encodeURIComponent(selectedGareName)}`)}
className="hover:underline text-[13px] font-semibold inline-flex items-center gap-1"
style={{ color: theme.primary }}
>
<span>Retrouvez les fiches horaires et travaux</span>
<span>›</span>
</Link>
</div>
{/* 8. SECTION INSPIRATION DE VOYAGES */}
<div className="max-w-[1240px] mx-auto px-4 sm:px-6 pt-4 pb-8 border-t border-gray-200">
<h3 className="text-[15px] font-semibold text-gray-800 text-center mb-6">
Inspiration de voyages en TER {theme.regionName}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 text-[12.5px] text-gray-600 text-center">
{/* Column 1: Destinations favorites */}
<div>
<h4 className="font-bold text-gray-900 mb-3 text-[13px]">Destinations favorites</h4>
<ul className="space-y-2">
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Besancon')} className="hover:underline hover:text-black">Dijon &gt; Besançon</Link></li>
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Laroche-Migennes')} className="hover:underline hover:text-black">Dijon &gt; Laroche-Migennes</Link></li>
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Chalon-sur-Saone')} className="hover:underline hover:text-black">Dijon &gt; Chalon-sur-Saône</Link></li>
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Paris')} className="hover:underline hover:text-black">Dijon &gt; Paris</Link></li>
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Dole')} className="hover:underline hover:text-black">Dijon &gt; Dole</Link></li>
</ul>
</div>
{/* Column 2: Gares à proximité */}
<div>
<h4 className="font-bold text-gray-900 mb-3 text-[13px]">Gares à proximité de {selectedGareName}</h4>
<ul className="space-y-2">
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Dijon-Porte-Neuve')} className="hover:underline hover:text-black">Dijon-Porte-Neuve</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Genlis')} className="hover:underline hover:text-black">Genlis</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Ouges')} className="hover:underline hover:text-black">Ouges</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Gevrey-Chambertin')} className="hover:underline hover:text-black">Gevrey-Chambertin</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Vougeot')} className="hover:underline hover:text-black">Vougeot</Link></li>
</ul>
</div>
{/* Column 3: Gares les plus recherchées */}
<div>
<h4 className="font-bold text-gray-900 mb-3 text-[13px]">Gares les plus recherchées</h4>
<ul className="space-y-2">
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Paris-Bercy')} className="hover:underline hover:text-black">Paris Bercy</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Besancon-Viotte')} className="hover:underline hover:text-black">Besançon Viotte</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Laroche-Migennes')} className="hover:underline hover:text-black">Laroche-Migennes</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Sens')} className="hover:underline hover:text-black">Sens</Link></li>
<li><Link href={getRegionPath('/se-deplacer/gares?gare=Belfort')} className="hover:underline hover:text-black">Belfort</Link></li>
</ul>
</div>
</div>
</div>
</main>
)}
</div>
{/* MODAL: Voir toutes les gares */}
{showAllStationsModal && (
<div
className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4 animate-fadeIn"
onClick={() => setShowAllStationsModal(false)}
>
<div
className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="font-bold text-[16px] text-gray-900">Liste des gares du réseau</h3>
<button
type="button"
onClick={() => setShowAllStationsModal(false)}
className="text-gray-400 hover:text-black p-1 cursor-pointer"
>
<span className="material-icons text-[20px]">close</span>
</button>
</div>
<div className="p-5 overflow-y-auto grid grid-cols-1 sm:grid-cols-2 gap-2 text-[13px]">
{garesList.map((g) => (
<button
key={g.id}
type="button"
onClick={() => {
setShowAllStationsModal(false);
setSearchInput(g.nom);
handleSearch(g.nom);
}}
className="text-left py-1.5 px-2 hover:bg-gray-50 rounded transition-colors"
style={{ color: theme.primary }}
>
{g.nom}
</button>
))}
</div>
</div>
</div>
)}
{/* REUSABLE SITE FOOTER (Image 1) */}
<SiteFooter regionSlug={regionSlug} />
</div>
);
}
+144 -106
View File
@@ -44,7 +44,22 @@ const DEFAULT_MENU: MenuItem[] = [
{ {
id: 'def-1-3', id: 'def-1-3',
label: 'Gares', label: 'Gares',
url: '/#gares' url: '/se-deplacer/gares'
},
{
id: 'def-1-4',
label: 'Fiches horaires',
url: '/se-deplacer/fiches-horaires'
},
{
id: 'def-1-5',
label: 'Info trafic',
url: '/se-deplacer/info-trafic',
sous_elements: [
{ id: 'def-1-5-1', label: 'Info trafic général', url: '/se-deplacer/info-trafic' },
{ id: 'def-1-5-2', label: 'Information travaux', url: '/se-deplacer/info-trafic/info-travaux' },
{ id: 'def-1-5-3', label: 'Information grève', url: '/se-deplacer/info-trafic/info-greves' }
]
} }
], ],
sous_sous_elements: [] sous_sous_elements: []
@@ -57,16 +72,16 @@ const DEFAULT_MENU: MenuItem[] = [
{ {
id: 'def-2-1', id: 'def-2-1',
label: 'Fiches horaires', label: 'Fiches horaires',
url: '/#fiches-horaires' url: '/se-deplacer/fiches-horaires'
}, },
{ {
id: 'def-2-2', id: 'def-2-2',
label: 'Info trafic', label: 'Info trafic',
url: '/#info-trafic', url: '/se-deplacer/info-trafic',
sous_elements: [ sous_elements: [
{ id: 'def-2-2-1', label: 'Travaux toutes lignes', url: '/#info-trafic' }, { id: 'def-2-2-1', label: 'Info trafic général', url: '/se-deplacer/info-trafic' },
{ id: 'def-2-2-2', label: 'Travaux ligne Paris - Laroche - Dijon - Lyon', url: '/#info-trafic' }, { id: 'def-2-2-2', label: 'Information travaux', url: '/se-deplacer/info-trafic/info-travaux' },
{ id: 'def-2-2-3', label: 'Adaptation axe Lyon - Paray-le-Monial - Nevers', url: '/#info-trafic' } { id: 'def-2-2-3', label: 'Information grève', url: '/se-deplacer/info-trafic/info-greves' }
] ]
} }
] ]
@@ -204,11 +219,20 @@ export default function HeaderMenu({
return list; return list;
}; };
const chunkSubItemsIntoColumns = (subItems: MenuItem[], maxPerCol = 3): MenuItem[][] => {
if (!subItems || subItems.length === 0) return [];
const columns: MenuItem[][] = [];
for (let i = 0; i < subItems.length; i += maxPerCol) {
columns.push(subItems.slice(i, i + maxPerCol));
}
return columns;
};
const activeSubItems = activeItem ? getSubItems(activeItem) : []; const activeSubItems = activeItem ? getSubItems(activeItem) : [];
return ( return (
<div className="relative z-40 bg-white border-b border-gray-200 shadow-xs"> <div className="relative z-40 bg-white border-b border-gray-200 shadow-xs">
{/* DESKTOP NAVIGATION BAR (IMAGE 1 & 2) */} {/* DESKTOP NAVIGATION BAR */}
<nav className="hidden lg:block"> <nav className="hidden lg:block">
<div className="max-w-[1340px] mx-auto px-6 flex items-center gap-7 text-[13px] font-medium text-gray-700"> <div className="max-w-[1340px] mx-auto px-6 flex items-center gap-7 text-[13px] font-medium text-gray-700">
{items.map((item, index) => { {items.map((item, index) => {
@@ -239,16 +263,16 @@ export default function HeaderMenu({
}} }}
className={`inline-block transition-colors cursor-pointer border-b-[3px] -mb-[3.5px] pb-3 whitespace-nowrap ${ className={`inline-block transition-colors cursor-pointer border-b-[3px] -mb-[3.5px] pb-3 whitespace-nowrap ${
isActive isActive
? 'font-bold border-[#8da300] text-gray-900' ? 'font-bold text-gray-900'
: isHovered : isHovered
? 'border-[#d1d800] text-gray-900' ? 'text-gray-900'
: 'border-transparent text-gray-700 hover:text-gray-900' : 'border-transparent text-gray-700 hover:text-gray-900'
}`} }`}
style={ style={
isActive isActive
? { borderColor: '#8da300', color: '#111111' } ? { borderColor: theme.secondary || '#8da300', color: '#111111' }
: isHovered : isHovered
? { borderColor: '#d1d800' } ? { borderColor: theme.secondary || '#d1d800' }
: undefined : undefined
} }
> >
@@ -259,7 +283,7 @@ export default function HeaderMenu({
})} })}
</div> </div>
{/* DESKTOP MEGA DROPDOWN (ORGANISÉ EN COLONNES) */} {/* DESKTOP MEGA DROPDOWN (ORGANISÉ EN COLONNES: 3 MIN, 6 MAX PAR COLONNE) */}
{activeItem && activeSubItems.length > 0 && ( {activeItem && activeSubItems.length > 0 && (
<div <div
className="absolute left-0 right-0 top-full bg-white shadow-xl border-t border-gray-100 z-50 animate-fadeIn" className="absolute left-0 right-0 top-full bg-white shadow-xl border-t border-gray-100 z-50 animate-fadeIn"
@@ -270,109 +294,31 @@ export default function HeaderMenu({
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<div className="max-w-[1340px] mx-auto px-6 py-6"> <div className="max-w-[1340px] mx-auto px-6 py-6">
{/* Disposition par colonnes verticales */} <div className="flex flex-row flex-wrap gap-12 items-start">
<div className="grid grid-flow-col auto-cols-fr gap-8 items-start"> {chunkSubItemsIntoColumns(activeSubItems, 3).map((column, colIdx) => (
{activeSubItems.map((sub, sIdx) => { <div key={colIdx} className="flex flex-col space-y-4 min-w-[220px] max-w-[320px]">
const nestedList = getSubItems(sub); {column.map((sub, sIdx) => {
return (
<div key={sub.id || sIdx} className="flex flex-col space-y-3 min-w-[200px]">
<Link
href={resolveUrl(sub.url)}
className="text-[14px] font-semibold text-gray-900 transition-colors block border-b pb-1.5"
style={{ borderColor: `${theme.secondary}40`, color: theme.primary }}
>
{sub.label}
</Link>
{nestedList.length > 0 && (
<div
className="pl-3 space-y-2 mt-1 flex flex-col border-l-2"
style={{ borderColor: theme.secondary || '#d1d800' }}
>
{nestedList.map((nested, nIdx) => (
<Link
key={nested.id || nIdx}
href={resolveUrl(nested.url)}
className="text-[12.5px] text-gray-700 hover:text-black block transition-colors leading-relaxed font-normal"
>
{nested.label}
</Link>
))}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
)}
</nav>
{/* MOBILE BAR & ACCORDION DRAWER */}
<div className="lg:hidden flex items-center justify-between px-4 py-2.5 bg-gray-50 border-t border-gray-200">
<span className="text-[13px] font-semibold text-gray-800">Menu Régional</span>
<button
onClick={() => setMobileMenuOpen(!isMobileMenuOpen)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 text-xs font-medium cursor-pointer shadow-xs active:bg-gray-100"
>
<span className="material-icons text-[18px]">
{isMobileMenuOpen ? 'close' : 'menu'}
</span>
<span>{isMobileMenuOpen ? 'Fermer' : 'Navigation'}</span>
</button>
</div>
{isMobileMenuOpen && (
<div className="lg:hidden bg-white border-b border-gray-200 px-4 py-3 space-y-2 animate-fadeIn">
{items.map((item, idx) => {
const subItems = getSubItems(item);
const isExpanded = !!expandedMobileIndices[idx];
return (
<div key={item.id || idx} className="border-b border-gray-100 pb-2">
<div className="flex items-center justify-between">
<Link
href={resolveUrl(item.url)}
onClick={() => setMobileMenuOpen(false)}
className="text-[14px] font-semibold text-gray-800 hover:text-[#006837] py-1"
>
{item.label}
</Link>
{subItems.length > 0 && (
<button
onClick={() => toggleMobileIndex(idx)}
className="p-1 text-gray-500 hover:text-gray-800"
>
<span className="material-icons text-[20px]">
{isExpanded ? 'expand_less' : 'expand_more'}
</span>
</button>
)}
</div>
{isExpanded && subItems.length > 0 && (
<div className="pl-4 mt-2 space-y-2 border-l-2 border-[#d1d800]">
{subItems.map((sub, sIdx) => {
const nestedList = getSubItems(sub); const nestedList = getSubItems(sub);
return ( return (
<div key={sub.id || sIdx} className="space-y-1"> <div key={sub.id || sIdx} className="space-y-1.5">
<Link <Link
href={resolveUrl(sub.url)} href={resolveUrl(sub.url)}
onClick={() => setMobileMenuOpen(false)} className="text-[13.5px] font-semibold text-gray-900 transition-colors block hover:underline"
className="text-[13px] font-medium text-gray-700 hover:text-[#006837] block py-0.5" style={{ color: theme.primary }}
> >
{sub.label} {sub.label}
</Link> </Link>
{nestedList.length > 0 && ( {nestedList.length > 0 && (
<div className="pl-3 space-y-1 border-l border-gray-200"> <div
className="pl-3 space-y-1.5 mt-1 flex flex-col border-l-2"
style={{ borderColor: theme.secondary || '#d1d800' }}
>
{nestedList.map((nested, nIdx) => ( {nestedList.map((nested, nIdx) => (
<Link <Link
key={nested.id || nIdx} key={nested.id || nIdx}
href={resolveUrl(nested.url)} href={resolveUrl(nested.url)}
onClick={() => setMobileMenuOpen(false)} className="text-[12px] text-gray-600 hover:text-black block transition-colors leading-relaxed font-normal"
className="text-[12px] text-gray-500 hover:text-gray-800 block py-0.5"
> >
{nested.label} {nested.label}
</Link> </Link>
@@ -383,10 +329,102 @@ export default function HeaderMenu({
); );
})} })}
</div> </div>
)} ))}
</div> </div>
); </div>
})} </div>
)}
</nav>
{/* ─── MOBILE FULLSCREEN MENU (STRICTLY CONFORME IMG 2) ─── */}
{isMobileMenuOpen && (
<div className="lg:hidden fixed inset-0 z-50 bg-white flex flex-col justify-between overflow-y-auto animate-fadeIn">
<div>
{/* Top Bar: ✕ Menu (Img 2) */}
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-gray-200">
<button
type="button"
onClick={() => setMobileMenuOpen(false)}
className="text-[#006837] flex items-center justify-center p-0.5 cursor-pointer"
aria-label="Fermer le menu"
>
<span className="material-icons text-[24px] font-bold">close</span>
</button>
<span className="text-[16.5px] font-semibold text-[#006837]">Menu</span>
</div>
{/* Navigation List Items with chevrons (Img 2) */}
<div className="divide-y divide-gray-100">
{items.map((item, idx) => {
const subItems = getSubItems(item);
const isExpanded = !!expandedMobileIndices[idx];
return (
<div key={item.id || idx}>
<div
className="px-4 py-3.5 flex items-center justify-between text-[14px] text-gray-800 hover:bg-gray-50 cursor-pointer"
onClick={() => {
if (subItems.length > 0) {
toggleMobileIndex(idx);
} else {
setMobileMenuOpen(false);
}
}}
>
<Link
href={resolveUrl(item.url)}
onClick={(e) => {
if (subItems.length > 0) {
e.preventDefault();
toggleMobileIndex(idx);
} else {
setMobileMenuOpen(false);
}
}}
className="flex-1 text-gray-800 font-normal"
>
{item.label}
</Link>
<span className="material-icons text-[#006837] text-[18px]">
{subItems.length > 0 && isExpanded ? 'expand_less' : 'chevron_right'}
</span>
</div>
{/* Sub-items if expanded */}
{isExpanded && subItems.length > 0 && (
<div className="bg-gray-50 px-6 py-2 divide-y divide-gray-100">
{subItems.map((sub, sIdx) => (
<Link
key={sub.id || sIdx}
href={resolveUrl(sub.url)}
onClick={() => setMobileMenuOpen(false)}
className="block py-2.5 text-[13px] text-[#006837] font-medium hover:underline"
>
{sub.label}
</Link>
))}
</div>
)}
</div>
);
})}
</div>
</div>
{/* Bottom Link: Mes commandes (Img 2) */}
<div className="border-t border-gray-200 bg-white p-2">
<Link
href={`/${regionSlug}/mes-commandes`}
onClick={() => setMobileMenuOpen(false)}
className="flex items-center justify-between text-[#006837] text-[13.5px] font-normal py-3 px-4 hover:bg-gray-50 rounded-lg cursor-pointer"
>
<div className="flex items-center gap-2.5">
<span className="material-icons text-[18px]">receipt_long</span>
<span>Mes commandes</span>
</div>
<span className="material-icons text-[18px]">chevron_right</span>
</Link>
</div>
</div> </div>
)} )}
</div> </div>
+44 -40
View File
@@ -4,6 +4,7 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { WcsButton, WcsCard, WcsCardBody } from 'wcs-react'; import { WcsButton, WcsCard, WcsCardBody } from 'wcs-react';
import ClientWcsProvider from '@/components/ClientWcsProvider'; import ClientWcsProvider from '@/components/ClientWcsProvider';
import SiteFooter from '@/components/SiteFooter';
interface RegionData { interface RegionData {
nom_site: string; nom_site: string;
@@ -19,48 +20,51 @@ interface GeneralParams {
export default function HomeContent({ general }: { general: GeneralParams | null }) { export default function HomeContent({ general }: { general: GeneralParams | null }) {
return ( return (
<ClientWcsProvider> <ClientWcsProvider>
<div className="flex flex-col items-center min-h-screen bg-zinc-50 dark:bg-zinc-900 p-8"> <div className="flex flex-col min-h-screen bg-zinc-50 dark:bg-zinc-900 justify-between">
<div className="w-full max-w-4xl space-y-8"> <div className="flex flex-col items-center p-8">
<div className="text-center"> <div className="w-full max-w-4xl space-y-8">
<h1 className="text-4xl font-bold text-zinc-900 dark:text-zinc-50 mb-4"> <div className="text-center">
FERROVIA Regional App <h1 className="text-4xl font-bold text-zinc-900 dark:text-zinc-50 mb-4">
</h1> FERROVIA Regional App
<p className="text-lg text-zinc-600 dark:text-zinc-400"> </h1>
Découvrez les horaires, les trajets et les informations trafic de votre région. <p className="text-lg text-zinc-600 dark:text-zinc-400">
Cette application unifie les sites régionaux TER avec une interface moderne. Découvrez les horaires, les trajets et les informations trafic de votre région.
</p> Cette application unifie les sites régionaux TER avec une interface moderne.
</div> </p>
{!general ? (
<WcsCard>
<WcsCardBody>
<p className="text-center text-red-500">
Impossible de charger la configuration depuis Supabase.
</p>
</WcsCardBody>
</WcsCard>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.entries(general.regions).map(([slug, regionInfo]) => (
<WcsCard key={slug} className="flex flex-col">
<WcsCardBody className="flex flex-col h-full justify-between items-center text-center p-6 gap-4">
<h2 className="text-xl font-semibold">
{regionInfo.nom_site || `FERROVIA ${slug}`}
</h2>
<p className="text-sm text-zinc-500">
Accédez au portail régional pour {slug.replace(/-/g, ' ')}.
</p>
<Link href={`/${slug}`} passHref legacyBehavior>
<WcsButton className="wcs-primary mt-4 w-full">
Accéder
</WcsButton>
</Link>
</WcsCardBody>
</WcsCard>
))}
</div> </div>
)}
{!general ? (
<WcsCard>
<WcsCardBody>
<p className="text-center text-red-500">
Impossible de charger la configuration depuis Supabase.
</p>
</WcsCardBody>
</WcsCard>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.entries(general.regions).map(([slug, regionInfo]) => (
<WcsCard key={slug} className="flex flex-col">
<WcsCardBody className="flex flex-col h-full justify-between items-center text-center p-6 gap-4">
<h2 className="text-xl font-semibold">
{regionInfo.nom_site || `FERROVIA ${slug}`}
</h2>
<p className="text-sm text-zinc-500">
Accédez au portail régional pour {slug.replace(/-/g, ' ')}.
</p>
<Link href={`/${slug}`} passHref legacyBehavior>
<WcsButton className="wcs-primary mt-4 w-full">
Accéder
</WcsButton>
</Link>
</WcsCardBody>
</WcsCard>
))}
</div>
)}
</div>
</div> </div>
<SiteFooter />
</div> </div>
</ClientWcsProvider> </ClientWcsProvider>
); );
+23 -90
View File
@@ -3,9 +3,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import SiteHeader from '@/components/SiteHeader'; import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
import TimetableSearchForm from '@/components/TimetableSearchForm'; import TimetableSearchForm from '@/components/TimetableSearchForm';
import ClientWcsProvider from '@/components/ClientWcsProvider'; import ClientWcsProvider from '@/components/ClientWcsProvider';
import { getRegionTheme } from '@/lib/regionTheme';
interface HorairesPageClientProps { interface HorairesPageClientProps {
regionSlug: string; regionSlug: string;
regionData: { regionData: {
@@ -16,84 +19,19 @@ interface HorairesPageClientProps {
menuStructure?: any; menuStructure?: any;
} }
const REGION_CONFIGS: Record<string, { heroImage: string; logoUrl: string; name: string }> = {
'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({ export default function HorairesPageClient({
regionSlug, regionSlug = 'bourgogne-franche-comte',
regionData, regionData,
menuStructure, menuStructure,
}: HorairesPageClientProps) { }: HorairesPageClientProps) {
const cfg = REGION_CONFIGS[regionSlug] || { const theme = getRegionTheme(regionSlug);
heroImage: DEFAULT_HERO,
logoUrl: regionData.logo_header_url || '',
name: regionSlug.replace(/-/g, ' ').toUpperCase()
};
const logoUrl = regionData.logo_header_url || cfg.logoUrl; const regionName = regionData?.nom_site
const heroImage = cfg.heroImage; ? regionData.nom_site.replace('FERROVIA ', '').trim()
: regionSlug.replace(/-/g, ' ').toUpperCase();
const logoUrl = regionData?.logo_header_url || '';
const heroImage = theme.heroImage;
const [searchFeedback, setSearchFeedback] = useState<string | null>(null); const [searchFeedback, setSearchFeedback] = useState<string | null>(null);
@@ -121,7 +59,7 @@ export default function HorairesPageClient({
<div className="bg-white text-xs py-2.5 px-4 sm:px-6"> <div className="bg-white text-xs py-2.5 px-4 sm:px-6">
<div className="max-w-[1340px] mx-auto flex items-center gap-2 text-gray-600"> <div className="max-w-[1340px] mx-auto flex items-center gap-2 text-gray-600">
<Link href={`/${regionSlug}`} className="hover:text-black flex items-center gap-1"> <Link href={`/${regionSlug}`} className="hover:text-black flex items-center gap-1">
<span className="material-icons text-[17px] text-[#006837]">home</span> <span className="material-icons text-[17px]" style={{ color: theme.primary }}>home</span>
</Link> </Link>
<span className="text-gray-400">&gt;</span> <span className="text-gray-400">&gt;</span>
<span className="font-medium text-gray-800">Rechercher un horaire</span> <span className="font-medium text-gray-800">Rechercher un horaire</span>
@@ -137,34 +75,29 @@ export default function HorairesPageClient({
> >
<div className="max-w-[1340px] mx-auto relative z-10"> <div className="max-w-[1340px] mx-auto relative z-10">
{searchFeedback && ( {searchFeedback && (
<div className="mb-4 bg-white/95 border-l-4 border-[#006837] text-gray-900 px-4 py-3 rounded-lg shadow-md flex justify-between items-center text-sm font-medium"> <div
className="mb-4 bg-white/95 text-gray-900 px-4 py-3 rounded-lg shadow-md flex justify-between items-center text-sm font-medium border-l-4"
style={{ borderColor: theme.primary }}
>
<span>{searchFeedback}</span> <span>{searchFeedback}</span>
<button onClick={() => setSearchFeedback(null)} className="text-gray-500 hover:text-black font-bold">✕</button> <button onClick={() => setSearchFeedback(null)} className="text-gray-500 hover:text-black font-bold cursor-pointer">✕</button>
</div> </div>
)} )}
{/* Timetable Search Form card pinned to LEFT side (Image 1) */} {/* Timetable Search Form card pinned to LEFT side (Image 1) */}
<div className="w-full lg:w-[440px]"> <div className="w-full lg:w-[440px]">
<TimetableSearchForm onSearch={handleSearchResults} /> <TimetableSearchForm
regionSlug={regionSlug}
onSearch={handleSearchResults}
/>
</div> </div>
</div> </div>
</section> </section>
{/* ================================================================== */} {/* ================================================================== */}
{/* 5. FOOTER */} {/* 5. FOOTER (IMAGE 1) */}
{/* ================================================================== */} {/* ================================================================== */}
<footer className="bg-white border-t border-gray-200 py-8 px-4 sm:px-6 text-center text-xs text-gray-500 space-y-4"> <SiteFooter regionSlug={regionSlug} />
<div className="flex flex-wrap justify-center gap-4 text-gray-600 font-medium">
<Link href={`/${regionSlug}`} className="hover:underline">Accueil {cfg.name}</Link>
<span>•</span>
<Link href={`/${regionSlug}/horaires`} className="hover:underline">Rechercher un horaire</Link>
<span>•</span>
<Link href={`/${regionSlug}#info-trafic`} className="hover:underline">Info Trafic</Link>
<span>•</span>
<Link href={`/${regionSlug}#actualites`} className="hover:underline">Actualités</Link>
</div>
<p>© FERROVIA - Tous droits réservés.</p>
</footer>
</div> </div>
</ClientWcsProvider> </ClientWcsProvider>
+323
View File
@@ -0,0 +1,323 @@
"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
interface InfoGrevesClientProps {
regionSlug?: string;
}
export default function InfoGrevesClient({
regionSlug = 'bourgogne-franche-comte'
}: InfoGrevesClientProps) {
const theme = getRegionTheme(regionSlug);
const [menuStructure, setMenuStructure] = useState<any>(null);
const [affluenceModalOpen, setAffluenceModalOpen] = useState(false);
useEffect(() => {
const fetchMenu = async () => {
try {
const cleanSlug = regionSlug || 'bourgogne-franche-comte';
const { data: rData } = await supabase
.from('regions')
.select('id, slug')
.or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`);
const rIds = rData ? rData.map((r: any) => r.id) : [];
let query = supabase.from('menus_regionaux').select('structure').eq('actif', true);
if (rIds.length > 0) {
query = query.in('region_id', rIds);
}
const { data: mData } = await query.limit(1);
if (mData && mData.length > 0 && mData[0].structure) {
setMenuStructure(mData[0].structure);
}
} catch (e) {
console.error('Error fetching regional menu:', e);
}
};
fetchMenu();
}, [regionSlug]);
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
return (
<div className="min-h-screen bg-white text-gray-900 flex flex-col justify-between font-sans">
<div>
{/* SITE HEADER */}
<SiteHeader regionSlug={regionSlug} menuStructure={menuStructure} />
{/* ─── MAIN CONTENT ─── */}
<main className="max-w-[1240px] mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Breadcrumbs */}
<nav className="text-[12.5px] text-gray-500 mb-6 flex items-center gap-1.5 font-medium flex-wrap">
<Link href={getRegionPath('')} className="hover:underline text-gray-600">
Accueil
</Link>
<span className="text-gray-400">›</span>
<Link href={getRegionPath('/se-deplacer/info-trafic')} className="hover:underline text-gray-600">
Information trafic, travaux, grève
</Link>
<span className="text-gray-400">›</span>
<span className="text-gray-800 font-semibold">Information grève</span>
</nav>
{/* ─── TOP HERO BANNER (Img 3) ─── */}
<div
className="rounded-2xl p-6 sm:p-10 text-white mb-10 shadow-sm relative overflow-hidden"
style={{ backgroundColor: theme.primary }}
>
<div className="max-w-[850px] relative z-10">
<span className="inline-block bg-white/20 text-white text-[12px] font-bold uppercase tracking-wider px-3 py-1 rounded-full mb-3 backdrop-blur-xs">
Mouvement social & Prévisions
</span>
<h1 className="text-[24px] sm:text-[32px] md:text-[36px] font-black uppercase tracking-tight leading-tight mb-3">
GRÈVE : LA CIRCULATION DES TRAINS {theme.operatorLabel?.toUpperCase() || 'MOBIGO'}
</h1>
<p className="text-[14.5px] sm:text-[16px] text-white/90 leading-relaxed font-light">
Retrouvez les prévisions de circulation et l&apos;ensemble des informations pratiques pour vos voyages lors des mouvements sociaux.
</p>
</div>
{/* Background Decorative Icon */}
<div className="absolute right-4 -bottom-6 opacity-10 pointer-events-none select-none hidden md:block">
<span className="material-icons text-[180px]">campaign</span>
</div>
</div>
{/* ─── SECTION 1: LES PERTURBATIONS SUR VOTRE LIGNE (Img 3) ─── */}
<section className="mb-12 bg-[#fafafa] rounded-2xl p-6 sm:p-8 border border-gray-200">
<h2 className="text-[20px] sm:text-[22px] font-black text-gray-900 uppercase tracking-tight mb-6 flex items-center gap-2.5">
<span className="w-2.5 h-6 rounded-full" style={{ backgroundColor: theme.primary }}></span>
<span>LES PERTURBATIONS SUR VOTRE LIGNE</span>
</h2>
<div className="bg-white rounded-xl p-6 border border-gray-200 shadow-xs mb-6">
<div className="flex items-start gap-4">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0 mt-0.5"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[22px]" style={{ color: theme.primary }}>
info
</span>
</div>
<div className="space-y-3 text-[14.5px] text-gray-700 leading-relaxed">
<p>
Lors d&apos;un préavis de grève, les plans de transport adaptés sont élaborés ligne par ligne pour vous assurer le meilleur niveau de service possible.
</p>
<p className="font-semibold text-gray-900">
🕒 Les prévisions de circulation détaillées pour le lendemain sont consultables chaque jour à partir de 17h00.
</p>
<p className="text-[13.5px] text-gray-600">
Nous vous recommandons vivement de vérifier la circulation effective de votre train ou car avant de vous déplacer en gare.
</p>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-white p-5 rounded-xl border border-gray-200">
<div className="text-[14px] text-gray-700 font-medium">
Consultez la fiche horaire du jour ou recherchez votre numéro de train :
</div>
<Link
href={getRegionPath('/se-deplacer/prochains-departs')}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-full text-[13.5px] font-bold text-white transition-transform active:scale-95 shadow-xs hover:opacity-95 cursor-pointer whitespace-nowrap"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[18px]">search</span>
<span>Vérifier un horaire</span>
</Link>
</div>
</section>
{/* ─── SECTION 2: CONDITIONS D'ÉCHANGE & REMBOURSEMENT ─── */}
<section className="mb-12 bg-white rounded-2xl p-6 sm:p-8 border border-gray-200 shadow-xs">
<h2 className="text-[19px] font-black text-gray-900 uppercase tracking-tight mb-4 flex items-center gap-2.5">
<span className="w-2.5 h-6 rounded-full" style={{ backgroundColor: theme.primary }}></span>
<span>ÉCHANGE ET REMBOURSEMENT</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-[14px] text-gray-700 leading-relaxed">
<div className="p-5 rounded-xl bg-gray-50 border border-gray-100">
<h3 className="font-bold text-gray-900 mb-2 flex items-center gap-2">
<span className="material-icons text-[18px]" style={{ color: theme.primary }}>confirmation_number</span>
<span>Billets unitaires</span>
</h3>
<p>
Si votre train est supprimé en raison d&apos;un mouvement social, votre billet est utilisable sur n&apos;importe quel autre train de la même journée, ou remboursable sans frais sur simple demande sur le canal d&apos;achat initial.
</p>
</div>
<div className="p-5 rounded-xl bg-gray-50 border border-gray-100">
<h3 className="font-bold text-gray-900 mb-2 flex items-center gap-2">
<span className="material-icons text-[18px]" style={{ color: theme.primary }}>card_membership</span>
<span>Abonnés régionaux</span>
</h3>
<p>
Pour les abonnés mensuels ou annuels, des mesures de compensation ou de dédommagement peuvent être mises en place par la Région en cas de perturbations prolongées.
</p>
</div>
</div>
</section>
{/* ─── SECTION 3: BESOIN D'INFOS SUPPLÉMENTAIRES ? (Img 3) ─── */}
<section className="mb-14">
<h2 className="text-[19px] font-black text-gray-900 uppercase tracking-tight mb-6">
BESOIN D&apos;INFOS SUPPLÉMENTAIRES ?
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Card Phone */}
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-xs flex items-start gap-4">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[24px]" style={{ color: theme.primary }}>
call
</span>
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-900 mb-1">
Par téléphone
</h3>
<p className="text-[13.5px] text-gray-600 leading-relaxed">
Contactez le centre de relation client au <strong>0 800 872 872</strong> (service & appel gratuits), du lundi au vendredi de 7h à 20h et le samedi de 9h à 17h.
</p>
</div>
</div>
{/* Card Twitter / X */}
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-xs flex items-start gap-4">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0"
style={{ backgroundColor: '#f0f2f5' }}
>
<span className="text-[20px] font-black text-gray-900">𝕏</span>
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-900 mb-1">
Sur X (ex-Twitter)
</h3>
<p className="text-[13.5px] text-gray-600 leading-relaxed">
Suivez le fil <strong>@{theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'}_Train</strong> sur X pour suivre l&apos;info trafic en direct pendant les grèves.
</p>
</div>
</div>
</div>
</section>
{/* ─── SECTION 4: VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR (Img 3) ─── */}
<section className="mb-8">
<h2 className="text-[18px] font-black text-gray-900 uppercase tracking-tight mb-6">
VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-[800px]">
{/* Card 1: Information Travaux */}
<div className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between">
<div
className="px-5 py-3 text-white font-bold text-[14px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
INFORMATION TRAVAUX
</div>
<div className="p-5 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[13.5px] text-gray-600 leading-relaxed mb-4 min-h-[40px]">
Consultez les dates et impacts des chantiers prévus sur vos lignes.
</p>
<div>
<Link
href={getRegionPath('/se-deplacer/info-trafic/info-travaux')}
className="inline-flex items-center justify-center px-5 py-2 rounded-full text-[13px] font-bold border transition-colors hover:bg-gray-50"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</Link>
</div>
</div>
</div>
{/* Card 2: Affluence Voyageurs */}
<div className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between">
<div
className="px-5 py-3 text-white font-bold text-[14px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
AFFLUENCE VOYAGEURS
</div>
<div className="p-5 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[13.5px] text-gray-600 leading-relaxed mb-4 min-h-[40px]">
Consultez les prévisions d&apos;affluence pour voyager en toute sérénité.
</p>
<div>
<button
type="button"
onClick={() => setAffluenceModalOpen(true)}
className="inline-flex items-center justify-center px-5 py-2 rounded-full text-[13px] font-bold border transition-colors hover:bg-gray-50 cursor-pointer"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</button>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
{/* Affluence Modal */}
{affluenceModalOpen && (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6 shadow-2xl animate-fadeIn text-center">
<div
className="w-14 h-14 rounded-full mx-auto flex items-center justify-center mb-4"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[28px]" style={{ color: theme.primary }}>
groups
</span>
</div>
<h3 className="text-[18px] font-bold text-gray-900 mb-2">
Affluence Voyageurs
</h3>
<p className="text-[14px] text-gray-600 mb-6 leading-relaxed">
Consultez l&apos;affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires.
</p>
<div className="flex justify-center gap-3">
<button
type="button"
onClick={() => setAffluenceModalOpen(false)}
className="px-6 py-2 rounded-full text-[13.5px] font-bold text-white transition-opacity hover:opacity-90"
style={{ backgroundColor: theme.primary }}
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* SITE FOOTER */}
<SiteFooter regionSlug={regionSlug} />
</div>
);
}
+203
View File
@@ -0,0 +1,203 @@
"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
interface InfoTraficHubClientProps {
regionSlug?: string;
}
export default function InfoTraficHubClient({
regionSlug = 'bourgogne-franche-comte'
}: InfoTraficHubClientProps) {
const theme = getRegionTheme(regionSlug);
const [menuStructure, setMenuStructure] = useState<any>(null);
const [affluenceModalOpen, setAffluenceModalOpen] = useState(false);
useEffect(() => {
const fetchMenu = async () => {
try {
const cleanSlug = regionSlug || 'bourgogne-franche-comte';
const { data: rData } = await supabase
.from('regions')
.select('id, slug')
.or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`);
const rIds = rData ? rData.map((r: any) => r.id) : [];
let query = supabase.from('menus_regionaux').select('structure').eq('actif', true);
if (rIds.length > 0) {
query = query.in('region_id', rIds);
}
const { data: mData } = await query.limit(1);
if (mData && mData.length > 0 && mData[0].structure) {
setMenuStructure(mData[0].structure);
}
} catch (e) {
console.error('Error fetching regional menu:', e);
}
};
fetchMenu();
}, [regionSlug]);
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
const cards = [
{
id: 'travaux',
title: 'INFORMATION TRAVAUX',
description: 'Consultez les dates et impacts des chantiers prévus sur vos lignes.',
link: getRegionPath('/se-deplacer/info-trafic/info-travaux'),
isExternal: false,
},
{
id: 'adaptations',
title: 'ADAPTATIONS EXCEPTIONNELLES',
description: 'Retrouvez les fiches horaires spécifiques ou les adaptations temporaires sur vos lignes.',
link: getRegionPath('/se-deplacer/fiches-horaires'),
isExternal: false,
},
{
id: 'affluence',
title: 'AFFLUENCE VOYAGEURS',
description: 'Consultez les prévisions d\'affluence à bord des trains pour voyager en toute sérénité.',
link: '#',
onClick: () => setAffluenceModalOpen(true),
isExternal: false,
},
{
id: 'greve',
title: 'INFORMATION GRÈVE',
description: 'Consultez les prévisions de circulation lors des mouvements sociaux.',
link: getRegionPath('/se-deplacer/info-trafic/info-greves'),
isExternal: false,
},
];
return (
<div className="min-h-screen bg-[#f8f9fa] text-gray-900 flex flex-col justify-between font-sans">
<div>
{/* SITE HEADER */}
<SiteHeader regionSlug={regionSlug} menuStructure={menuStructure} />
{/* ─── MAIN CONTENT CONTAINER ─── */}
<main className="max-w-[1240px] mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Breadcrumbs */}
<nav className="text-[12.5px] text-gray-500 mb-6 flex items-center gap-1.5 font-medium">
<Link href={getRegionPath('')} className="hover:underline text-gray-600">
Accueil
</Link>
<span className="text-gray-400">›</span>
<span className="text-gray-800 font-semibold">Information trafic, travaux, grève</span>
</nav>
{/* Page Title & Subtitle */}
<div className="mb-10 text-left">
<h1 className="text-[26px] sm:text-[32px] font-black text-gray-900 tracking-tight uppercase mb-2">
L&apos;INFO TRAFIC TRAIN {theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'}
</h1>
<p className="text-[14.5px] sm:text-[15.5px] text-gray-600 font-normal">
Retrouvez l&apos;ensemble des informations concernant la circulation des TRAINS {theme.operatorLabel || 'Mobigo'}.
</p>
</div>
{/* ─── 4 CARDS GRID (Img 1 Layout) ─── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-8 max-w-[1000px] mx-auto">
{cards.map((card) => (
<div
key={card.id}
className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between transition-all duration-200 hover:shadow-md"
>
{/* Green Card Header */}
<div
className="px-6 py-4 text-white font-bold text-[15px] sm:text-[16px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
{card.title}
</div>
{/* Card Body */}
<div className="p-6 sm:p-8 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[14px] sm:text-[15px] text-gray-700 leading-relaxed mb-6 font-normal min-h-[48px]">
{card.description}
</p>
<div className="flex justify-center">
{card.onClick ? (
<button
type="button"
onClick={card.onClick}
className="inline-flex items-center justify-center px-6 py-2.5 rounded-full text-[13.5px] font-bold transition-transform active:scale-95 shadow-xs border cursor-pointer hover:opacity-90"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</button>
) : (
<Link
href={card.link}
className="inline-flex items-center justify-center px-6 py-2.5 rounded-full text-[13.5px] font-bold transition-transform active:scale-95 shadow-xs border cursor-pointer hover:opacity-90"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</Link>
)}
</div>
</div>
</div>
))}
</div>
</main>
</div>
{/* Affluence Modal */}
{affluenceModalOpen && (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6 shadow-2xl animate-fadeIn text-center">
<div
className="w-14 h-14 rounded-full mx-auto flex items-center justify-center mb-4"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[28px]" style={{ color: theme.primary }}>
groups
</span>
</div>
<h3 className="text-[18px] font-bold text-gray-900 mb-2">
Affluence Voyageurs
</h3>
<p className="text-[14px] text-gray-600 mb-6 leading-relaxed">
Consultez l&apos;affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires.
</p>
<div className="flex justify-center gap-3">
<button
type="button"
onClick={() => setAffluenceModalOpen(false)}
className="px-6 py-2 rounded-full text-[13.5px] font-bold text-white transition-opacity hover:opacity-90"
style={{ backgroundColor: theme.primary }}
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* SITE FOOTER */}
<SiteFooter regionSlug={regionSlug} />
</div>
);
}
+541
View File
@@ -0,0 +1,541 @@
"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
interface LigneItem {
id: string;
nom: string;
code?: string;
couleur?: string;
}
interface TravauxItem {
id: string;
titre: string;
description?: string;
ligne_id?: string;
date_debut?: string;
date_fin?: string;
url_affiche_pdf?: string;
actif?: boolean;
type_travaux?: string;
}
interface InfoTravauxClientProps {
regionSlug?: string;
}
const DEFAULT_LIGNES: LigneItem[] = [
{ id: '1', nom: 'Dijon - Besançon - Belfort', code: 'Ligne 01', couleur: '#006837' },
{ id: '2', nom: 'Paris - Dijon - Lyon', code: 'Ligne 02', couleur: '#004d7a' },
{ id: '3', nom: 'Dijon - Nevers - Tours', code: 'Ligne 03', couleur: '#7b1fa2' },
{ id: '4', nom: 'Besançon - La Chaux-de-Fonds (Ligne des Horlogers)', code: 'Ligne 04', couleur: '#c2185b' },
{ id: '5', nom: 'Dole - Saint-Claude (Ligne des Hirondelles)', code: 'Ligne 05', couleur: '#d84315' },
{ id: '6', nom: 'Belfort - Épinal', code: 'Ligne 06', couleur: '#00838f' },
];
export default function InfoTravauxClient({
regionSlug = 'bourgogne-franche-comte'
}: InfoTravauxClientProps) {
const theme = getRegionTheme(regionSlug);
const [menuStructure, setMenuStructure] = useState<any>(null);
const [lignesList, setLignesList] = useState<LigneItem[]>(DEFAULT_LIGNES);
const [travauxList, setTravauxList] = useState<TravauxItem[]>([]);
const [selectedTravaux, setSelectedTravaux] = useState<TravauxItem | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [affluenceModalOpen, setAffluenceModalOpen] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const cleanSlug = regionSlug || 'bourgogne-franche-comte';
const { data: rData } = await supabase
.from('regions')
.select('id, slug')
.or(`slug.eq.${cleanSlug},code.eq.${cleanSlug}`);
const rIds = rData ? rData.map((r: any) => r.id) : [];
// 1. Fetch menu
let queryMenu = supabase.from('menus_regionaux').select('structure').eq('actif', true);
if (rIds.length > 0) {
queryMenu = queryMenu.in('region_id', rIds);
}
const { data: mData } = await queryMenu.limit(1);
if (mData && mData.length > 0 && mData[0].structure) {
setMenuStructure(mData[0].structure);
}
// 2. Fetch lignes
const { data: lData } = await supabase
.from('lignes')
.select('*')
.eq('actif', true)
.is('deleted_at', null)
.order('nom', { ascending: true });
if (lData && lData.length > 0) {
setLignesList(lData as LigneItem[]);
}
// 3. Fetch travaux
const { data: tData } = await supabase
.from('travaux')
.select('*')
.eq('actif', true)
.is('deleted_at', null)
.order('date_debut', { ascending: false });
if (tData && tData.length > 0) {
setTravauxList(tData as TravauxItem[]);
}
} catch (err) {
console.error('Error fetching info travaux data:', err);
}
};
fetchData();
}, [regionSlug]);
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
const handleOpenTravaux = (ligne: LigneItem) => {
const found = travauxList.find(t => t.ligne_id === ligne.id);
if (found) {
setSelectedTravaux(found);
} else {
// Default placeholder travaux info for this line
setSelectedTravaux({
id: `travaux-${ligne.id}`,
titre: `Travaux de maintenance et renouvellement de voie - ${ligne.nom}`,
description: `Des travaux de modernisation et d'entretien des voies ferrées sont en cours sur l'axe ${ligne.nom}. Les horaires de certains trains peuvent être modifiés et des cars de substitution sont mis en place.`,
date_debut: 'En cours',
date_fin: '31 décembre 2026',
ligne_id: ligne.id,
});
}
setModalOpen(true);
};
return (
<div className="min-h-screen bg-white text-gray-900 flex flex-col justify-between font-sans">
<div>
{/* SITE HEADER */}
<SiteHeader regionSlug={regionSlug} menuStructure={menuStructure} />
{/* ─── MAIN CONTENT ─── */}
<main className="max-w-[1240px] mx-auto px-4 sm:px-6 py-6 sm:py-8">
{/* Breadcrumbs */}
<nav className="text-[12.5px] text-gray-500 mb-6 flex items-center gap-1.5 font-medium flex-wrap">
<Link href={getRegionPath('')} className="hover:underline text-gray-600">
Accueil
</Link>
<span className="text-gray-400">›</span>
<Link href={getRegionPath('/se-deplacer/info-trafic')} className="hover:underline text-gray-600">
Information trafic, travaux, grève
</Link>
<span className="text-gray-400">›</span>
<span className="text-gray-800 font-semibold">Information travaux</span>
</nav>
{/* ─── TOP HERO BANNER (Img 2) ─── */}
<div
className="rounded-2xl p-6 sm:p-10 text-white mb-10 shadow-sm relative overflow-hidden"
style={{ backgroundColor: theme.primary }}
>
<div className="max-w-[850px] relative z-10">
<span className="inline-block bg-white/20 text-white text-[12px] font-bold uppercase tracking-wider px-3 py-1 rounded-full mb-3 backdrop-blur-xs">
Info Réseau & Chantiers
</span>
<h1 className="text-[24px] sm:text-[32px] md:text-[36px] font-black uppercase tracking-tight leading-tight mb-3">
LES TRAVAUX : TOUT SAVOIR POUR VOS VOYAGES EN TRAIN {theme.operatorLabel?.toUpperCase() || 'MOBIGO'}
</h1>
<p className="text-[14.5px] sm:text-[16px] text-white/90 leading-relaxed font-light">
Consultez les adaptations et retrouvez vos trains / cars {theme.operatorLabel || 'Mobigo'} non concernés par les travaux sur les lignes de {theme.regionName || 'Bourgogne-Franche-Comté'}.
</p>
</div>
{/* Background Decorative Icon */}
<div className="absolute right-4 -bottom-6 opacity-10 pointer-events-none select-none hidden md:block">
<span className="material-icons text-[180px]">construction</span>
</div>
</div>
{/* ─── SECTION 1: COMPRENDRE LES TRAVAUX (Img 2) ─── */}
<section className="mb-12 bg-[#fafafa] rounded-2xl p-6 sm:p-8 border border-gray-200">
<h2 className="text-[20px] sm:text-[22px] font-black text-gray-900 uppercase tracking-tight mb-6 flex items-center gap-2.5">
<span className="w-2.5 h-6 rounded-full" style={{ backgroundColor: theme.primary }}></span>
<span>COMPRENDRE LES TRAVAUX</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-center">
<div className="md:col-span-7 space-y-4 text-[14.5px] text-gray-700 leading-relaxed">
<p>
<strong>SNCF Réseau</strong> réalise chaque année des centaines de chantiers de modernisation et de régénération des voies, des caténaires, de la signalisation et des ouvrages d&apos;art sur le réseau ferré régional.
</p>
<p>
Ces travaux indispensables permettent de <strong>renforcer la sécurité</strong>, d&apos;<strong>améliorer la régularité</strong> de vos trains du quotidien et de préparer le réseau aux mobilités futures.
</p>
<p>
Pour réaliser ces opérations en toute sécurité, la circulation ferroviaire est ponctuellement interrompue ou ralentie. Des <strong>plans de transport adaptés</strong> et des <strong>substitutions routières par autocar</strong> sont systématiquement mis en place pour assurer la continuité de vos trajets.
</p>
</div>
<div className="md:col-span-5">
<div className="relative rounded-xl overflow-hidden shadow-sm border border-gray-200 aspect-16/10 bg-gray-100 flex items-center justify-center group">
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent z-10"></div>
{/* High quality rail maintenance photo */}
<img
src="https://images.unsplash.com/photo-1541888946425-d0fbb18086f6?auto=format&fit=crop&w=800&q=80"
alt="Chantier travaux ferroviaires"
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute bottom-3 left-4 right-4 z-20 text-white">
<span className="text-[12px] font-bold tracking-wide uppercase bg-black/50 px-2.5 py-1 rounded-md backdrop-blur-xs">
Modernisation des voies ferrées
</span>
</div>
</div>
</div>
</div>
</section>
{/* ─── SECTION 2: IMPACTS SUR LES CIRCULATIONS & VÉRIFIER HORAIRE (Img 2) ─── */}
<section className="mb-12 bg-white rounded-2xl p-6 sm:p-8 border border-gray-200 shadow-xs flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div className="max-w-[700px]">
<h2 className="text-[18px] sm:text-[20px] font-black text-gray-900 uppercase tracking-tight mb-2">
IMPACTS SUR LES CIRCULATIONS
</h2>
<p className="text-[14px] text-gray-600 leading-relaxed">
Avant votre départ, nous vous recommandons de vérifier si votre train habituel circule ou s&apos;il est remplacé par un autocar de substitution.
</p>
</div>
<div>
<Link
href={getRegionPath('/se-deplacer/prochains-departs')}
className="inline-flex items-center gap-2 px-6 py-3 rounded-full text-[14px] font-bold text-white transition-all transform active:scale-95 shadow-md hover:opacity-95 cursor-pointer whitespace-nowrap"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[18px]">search</span>
<span>Vérifier un horaire</span>
</Link>
</div>
</section>
{/* ─── SECTION 3: ZOOM SUR LES PRINCIPAUX CHANTIERS (Img 2) ─── */}
<section className="mb-14">
<div className="mb-6">
<h2 className="text-[20px] sm:text-[22px] font-black text-gray-900 uppercase tracking-tight mb-1 flex items-center gap-2.5">
<span className="w-2.5 h-6 rounded-full" style={{ backgroundColor: theme.primary }}></span>
<span>ZOOM SUR LES PRINCIPAUX CHANTIERS</span>
</h2>
<p className="text-[14px] text-gray-500">
Consultez les affiches travaux détaillées et les fiches horaires modifiées par ligne :
</p>
</div>
<div className="bg-white rounded-2xl border border-gray-200 shadow-xs divide-y divide-gray-100 overflow-hidden">
{lignesList.map((ligne, idx) => {
const lineTravaux = travauxList.filter(t => t.ligne_id === ligne.id);
const hasTravaux = lineTravaux.length > 0;
return (
<div
key={ligne.id || idx}
className="p-4 sm:p-5 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-gray-50/80 transition-colors"
>
<div className="flex items-center gap-3.5">
<div
className="w-3.5 h-3.5 rounded-full shrink-0"
style={{ backgroundColor: ligne.couleur || theme.primary }}
/>
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-gray-900 text-[15px]">
{ligne.code ? `${ligne.code} : ` : ''}{ligne.nom}
</span>
</div>
<p className="text-[12.5px] text-gray-500 mt-0.5">
{hasTravaux
? `${lineTravaux.length} chantier(s) en cours ou programmé(s)`
: 'Travaux de maintenance réguliers'}
</p>
</div>
</div>
<div className="flex items-center gap-2 sm:self-center">
<button
type="button"
onClick={() => handleOpenTravaux(ligne)}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-full text-[13px] font-bold border transition-colors cursor-pointer hover:bg-gray-100"
style={{
color: '#556b2f',
borderColor: '#8da300',
backgroundColor: '#f6f8ee',
}}
>
<span className="material-icons text-[16px]">description</span>
<span>Affiches travaux</span>
</button>
</div>
</div>
);
})}
</div>
</section>
{/* ─── SECTION 4: BESOIN D'INFOS SUPPLÉMENTAIRES ? (Img 2) ─── */}
<section className="mb-14">
<h2 className="text-[19px] font-black text-gray-900 uppercase tracking-tight mb-6">
BESOIN D&apos;INFOS SUPPLÉMENTAIRES ?
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Card Phone */}
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-xs flex items-start gap-4">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[24px]" style={{ color: theme.primary }}>
call
</span>
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-900 mb-1">
Par téléphone
</h3>
<p className="text-[13.5px] text-gray-600 leading-relaxed">
Contactez le centre de relation client au <strong>0 800 872 872</strong> (service & appel gratuits), du lundi au vendredi de 7h à 20h et le samedi de 9h à 17h.
</p>
</div>
</div>
{/* Card Twitter / X */}
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-xs flex items-start gap-4">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0"
style={{ backgroundColor: '#f0f2f5' }}
>
<span className="text-[20px] font-black text-gray-900">𝕏</span>
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-900 mb-1">
Sur X (ex-Twitter)
</h3>
<p className="text-[13.5px] text-gray-600 leading-relaxed">
Suivez le fil <strong>@{theme.operatorLabel ? theme.operatorLabel.toUpperCase() : 'MOBIGO'}_Train</strong> sur X pour suivre l&apos;info trafic en temps réel 7j/7 de 6h à 20h.
</p>
</div>
</div>
</div>
</section>
{/* ─── SECTION 5: VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR (Img 2) ─── */}
<section className="mb-8">
<h2 className="text-[18px] font-black text-gray-900 uppercase tracking-tight mb-6">
VOUS POURRIEZ ÊTRE INTÉRESSÉS PAR
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Card 1: Information Grève */}
<div className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between">
<div
className="px-5 py-3 text-white font-bold text-[14px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
INFORMATION GRÈVE
</div>
<div className="p-5 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[13.5px] text-gray-600 leading-relaxed mb-4 min-h-[40px]">
Consultez les prévisions de circulation lors des mouvements sociaux.
</p>
<div>
<Link
href={getRegionPath('/se-deplacer/info-trafic/info-greves')}
className="inline-flex items-center justify-center px-5 py-2 rounded-full text-[13px] font-bold border transition-colors hover:bg-gray-50"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</Link>
</div>
</div>
</div>
{/* Card 2: Adaptations exceptionnelles */}
<div className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between">
<div
className="px-5 py-3 text-white font-bold text-[14px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
ADAPTATIONS EXCEPTIONNELLES
</div>
<div className="p-5 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[13.5px] text-gray-600 leading-relaxed mb-4 min-h-[40px]">
Retrouvez les fiches horaires spécifiques ou les adaptations temporaires.
</p>
<div>
<Link
href={getRegionPath('/se-deplacer/fiches-horaires')}
className="inline-flex items-center justify-center px-5 py-2 rounded-full text-[13px] font-bold border transition-colors hover:bg-gray-50"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</Link>
</div>
</div>
</div>
{/* Card 3: Affluence Voyageurs */}
<div className="bg-white rounded-xl shadow-xs border border-gray-200 overflow-hidden flex flex-col justify-between">
<div
className="px-5 py-3 text-white font-bold text-[14px] tracking-wide text-center"
style={{ backgroundColor: theme.primary }}
>
AFFLUENCE VOYAGEURS
</div>
<div className="p-5 flex flex-col justify-between flex-1 text-center bg-[#fafafa]">
<p className="text-[13.5px] text-gray-600 leading-relaxed mb-4 min-h-[40px]">
Consultez les prévisions d&apos;affluence pour voyager en toute sérénité.
</p>
<div>
<button
type="button"
onClick={() => setAffluenceModalOpen(true)}
className="inline-flex items-center justify-center px-5 py-2 rounded-full text-[13px] font-bold border transition-colors hover:bg-gray-50 cursor-pointer"
style={{
backgroundColor: '#ffffff',
color: theme.primary,
borderColor: theme.primary,
}}
>
Plus d&apos;infos
</button>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
{/* ─── TRAVAUX DETAIL MODAL ─── */}
{modalOpen && selectedTravaux && (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-lg w-full p-6 sm:p-7 shadow-2xl animate-fadeIn">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{ backgroundColor: '#f6f8ee', color: '#556b2f' }}
>
<span className="material-icons text-[22px]">construction</span>
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-900 leading-snug">
{selectedTravaux.titre}
</h3>
<span className="text-[12px] text-gray-500">
Période : {selectedTravaux.date_debut || 'En cours'} {selectedTravaux.date_fin ? `au ${selectedTravaux.date_fin}` : ''}
</span>
</div>
</div>
<button
type="button"
onClick={() => setModalOpen(false)}
className="text-gray-400 hover:text-gray-600 p-1 cursor-pointer"
>
<span className="material-icons">close</span>
</button>
</div>
<div className="my-5 p-4 rounded-xl bg-gray-50 border border-gray-100 text-[13.5px] text-gray-700 leading-relaxed">
{selectedTravaux.description}
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-end gap-3 pt-2">
{selectedTravaux.url_affiche_pdf ? (
<a
href={selectedTravaux.url_affiche_pdf}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-full text-[13.5px] font-bold text-white shadow-xs hover:opacity-95"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[18px]">download</span>
<span>Télécharger l&apos;affiche travaux (PDF)</span>
</a>
) : (
<Link
href={getRegionPath('/se-deplacer/fiches-horaires')}
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-full text-[13.5px] font-bold text-white shadow-xs hover:opacity-95"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[18px]">schedule</span>
<span>Voir les fiches horaires modifiées</span>
</Link>
)}
<button
type="button"
onClick={() => setModalOpen(false)}
className="px-5 py-2.5 rounded-full text-[13.5px] font-semibold text-gray-600 hover:bg-gray-100 transition-colors"
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* Affluence Modal */}
{affluenceModalOpen && (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6 shadow-2xl animate-fadeIn text-center">
<div
className="w-14 h-14 rounded-full mx-auto flex items-center justify-center mb-4"
style={{ backgroundColor: theme.primaryLight }}
>
<span className="material-icons text-[28px]" style={{ color: theme.primary }}>
groups
</span>
</div>
<h3 className="text-[18px] font-bold text-gray-900 mb-2">
Affluence Voyageurs
</h3>
<p className="text-[14px] text-gray-600 mb-6 leading-relaxed">
Consultez l&apos;affluence moyenne estimée de vos trains aux heures de pointe pour voyager dans les meilleures conditions. Cette information est également indiquée lors de la recherche de vos horaires.
</p>
<div className="flex justify-center gap-3">
<button
type="button"
onClick={() => setAffluenceModalOpen(false)}
className="px-6 py-2 rounded-full text-[13.5px] font-bold text-white transition-opacity hover:opacity-90"
style={{ backgroundColor: theme.primary }}
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* SITE FOOTER */}
<SiteFooter regionSlug={regionSlug} />
</div>
);
}
+59 -109
View File
@@ -4,7 +4,9 @@ import React, { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader'; import SiteHeader from '@/components/SiteHeader';
import SiteFooter from '@/components/SiteFooter';
interface OffersPageClientProps { interface OffersPageClientProps {
regionSlug?: string; regionSlug?: string;
@@ -175,6 +177,7 @@ export default function OffersPageClient({
}: OffersPageClientProps) { }: OffersPageClientProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const theme = getRegionTheme(regionSlug);
// Search parameters from URL // Search parameters from URL
const departure = searchParams.get('departure') || 'Dijon'; const departure = searchParams.get('departure') || 'Dijon';
@@ -524,7 +527,10 @@ export default function OffersPageClient({
}, [departure, arrival, outwardDate, outwardTime]); }, [departure, arrival, outwardDate, outwardTime]);
const handleModifySearch = () => { const handleModifySearch = () => {
const searchUrl = regionSlug ? `/${regionSlug}/horaires` : '/horaires'; const qp = searchParams.toString();
const searchUrl = regionSlug
? `/${regionSlug}/horaires${qp ? `?${qp}` : ''}`
: `/horaires${qp ? `?${qp}` : ''}`;
router.push(searchUrl); router.push(searchUrl);
}; };
@@ -706,7 +712,7 @@ export default function OffersPageClient({
{/* Train Icon & Number (Under Pagny on Desktop, Under Dijon on Mobile) */} {/* Train Icon & Number (Under Pagny on Desktop, Under Dijon on Mobile) */}
<div className="order-1 sm:order-2 flex items-center gap-1.5 text-[13px] font-semibold text-gray-800 md:ml-auto md:mr-12"> <div className="order-1 sm:order-2 flex items-center gap-1.5 text-[13px] font-semibold text-gray-800 md:ml-auto md:mr-12">
<span className="material-icons text-[#006837] text-[18px]">directions_railway</span> <span className="material-icons text-[18px]" style={{ color: theme.primary }}>directions_railway</span>
<span>{offer.trainType} {offer.trainNumber}</span> <span>{offer.trainType} {offer.trainNumber}</span>
</div> </div>
@@ -719,7 +725,8 @@ export default function OffersPageClient({
<button <button
type="button" type="button"
onClick={() => alert(`Billet sélectionné: Train ${offer.trainNumber} pour ${offer.price}`)} onClick={() => alert(`Billet sélectionné: Train ${offer.trainNumber} pour ${offer.price}`)}
className="bg-[#006837] hover:bg-[#00522b] text-white rounded-xl py-2.5 px-5 md:py-3 md:px-6 text-center shadow-xs cursor-pointer transition-transform active:scale-98 flex flex-col items-center justify-center min-w-[120px] md:min-w-[135px] w-full sm:w-auto" style={{ backgroundColor: theme.primary }}
className="text-white rounded-xl py-2.5 px-5 md:py-3 md:px-6 text-center shadow-xs cursor-pointer transition-transform active:scale-98 hover:opacity-90 flex flex-col items-center justify-center min-w-[120px] md:min-w-[135px] w-full sm:w-auto"
> >
<span className="text-[11px] font-normal opacity-90 leading-tight flex items-center gap-0.5"> <span className="text-[11px] font-normal opacity-90 leading-tight flex items-center gap-0.5">
2<sup className="text-[9px]">nde</sup> classe 2<sup className="text-[9px]">nde</sup> classe
@@ -734,7 +741,10 @@ export default function OffersPageClient({
{/* ─── EXPANDED ACCORDION DETAILS (STRICTLY CONFORME IMG 1) ─── */} {/* ─── EXPANDED ACCORDION DETAILS (STRICTLY CONFORME IMG 1) ─── */}
{isExpanded && ( {isExpanded && (
<div className="bg-[#eef3da] p-5 sm:p-6 rounded-b-2xl border-t border-[#d1d800]/40 flex flex-col lg:flex-row justify-between gap-6 animate-fadeIn"> <div
className="p-5 sm:p-6 rounded-b-2xl border-t flex flex-col lg:flex-row justify-between gap-6 animate-fadeIn"
style={{ backgroundColor: `${theme.primary}0f`, borderColor: `${theme.secondary || '#d1d800'}40` }}
>
{/* LEFT COLUMN: TIMELINE & TRAIN DETAILS (IMG 1) */} {/* LEFT COLUMN: TIMELINE & TRAIN DETAILS (IMG 1) */}
<div className="flex-1 space-y-4 font-sans text-gray-900"> <div className="flex-1 space-y-4 font-sans text-gray-900">
@@ -744,7 +754,7 @@ export default function OffersPageClient({
<span className="w-12 text-right font-bold text-[15.5px] text-gray-900"> <span className="w-12 text-right font-bold text-[15.5px] text-gray-900">
{offer.departureTime} {offer.departureTime}
</span> </span>
<span className="material-icons text-[20px] text-[#006837]">directions_railway</span> <span className="material-icons text-[20px]" style={{ color: theme.primary }}>directions_railway</span>
<span>{offer.departureStation}</span> <span>{offer.departureStation}</span>
</div> </div>
@@ -752,8 +762,14 @@ export default function OffersPageClient({
<div className="flex gap-3 pl-12"> <div className="flex gap-3 pl-12">
{/* Left vertical line with duration */} {/* Left vertical line with duration */}
<div className="w-8 flex flex-col items-center flex-shrink-0"> <div className="w-8 flex flex-col items-center flex-shrink-0">
<div className="w-[2px] bg-[#006837] flex-1 min-h-[140px] relative flex items-center justify-center"> <div
<span className="bg-[#eef3da] py-1 text-[12px] font-medium text-gray-700 whitespace-nowrap"> className="w-[2px] flex-1 min-h-[140px] relative flex items-center justify-center"
style={{ backgroundColor: theme.primary }}
>
<span
className="py-1 text-[12px] font-medium text-gray-700 whitespace-nowrap px-1 rounded"
style={{ backgroundColor: `${theme.primary}18` }}
>
{offer.duration} {offer.duration}
</span> </span>
</div> </div>
@@ -766,7 +782,8 @@ export default function OffersPageClient({
<button <button
type="button" type="button"
onClick={() => setSelectedModalOffer(offer)} onClick={() => setSelectedModalOffer(offer)}
className="text-[#006837] font-semibold text-[15px] underline cursor-pointer hover:opacity-80 block text-left" className="font-semibold text-[15px] underline cursor-pointer hover:opacity-80 block text-left"
style={{ color: theme.primary }}
> >
Train {offer.trainType} {offer.trainNumber} Train {offer.trainType} {offer.trainNumber}
</button> </button>
@@ -803,7 +820,8 @@ export default function OffersPageClient({
<button <button
type="button" type="button"
onClick={() => setSelectedModalOffer(offer)} onClick={() => setSelectedModalOffer(offer)}
className="text-[#006837] hover:bg-[#006837]/10 p-1.5 rounded-full cursor-pointer transition-colors" className="hover:opacity-80 p-1.5 rounded-full cursor-pointer transition-colors"
style={{ color: theme.primary }}
title="Voir les étapes du train" title="Voir les étapes du train"
> >
<span className="material-icons text-[28px] font-bold">chevron_right</span> <span className="material-icons text-[28px] font-bold">chevron_right</span>
@@ -830,7 +848,7 @@ export default function OffersPageClient({
<span className="w-12 text-right font-bold text-[15.5px] text-gray-900"> <span className="w-12 text-right font-bold text-[15.5px] text-gray-900">
{offer.arrivalTime} {offer.arrivalTime}
</span> </span>
<span className="material-icons text-[20px] text-[#006837]">place</span> <span className="material-icons text-[20px]" style={{ color: theme.primary }}>place</span>
<span>{offer.arrivalStation}</span> <span>{offer.arrivalStation}</span>
</div> </div>
@@ -843,7 +861,10 @@ export default function OffersPageClient({
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="w-10 h-10 rounded-full bg-[#006837] text-white flex items-center justify-center flex-shrink-0"> <span
className="w-10 h-10 rounded-full text-white flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[22px]">person</span> <span className="material-icons text-[22px]">person</span>
</span> </span>
<div> <div>
@@ -868,12 +889,13 @@ export default function OffersPageClient({
</div> </div>
</div> </div>
{/* Green price button at bottom right of card */} {/* Price button at bottom right of card */}
<div className="flex justify-end pt-2"> <div className="flex justify-end pt-2">
<button <button
type="button" type="button"
onClick={() => alert(`Billet réservé: Train ${offer.trainNumber} pour ${offer.price}`)} onClick={() => alert(`Billet réservé: Train ${offer.trainNumber} pour ${offer.price}`)}
className="bg-[#006837] text-white rounded-xl py-2.5 px-6 text-center min-w-[130px] shadow-xs hover:bg-[#00522b] cursor-pointer transition-transform active:scale-98 flex flex-col items-center justify-center" style={{ backgroundColor: theme.primary }}
className="text-white rounded-xl py-2.5 px-6 text-center min-w-[130px] shadow-xs hover:opacity-90 cursor-pointer transition-transform active:scale-98 flex flex-col items-center justify-center"
> >
<span className="text-[11px] font-normal opacity-90 leading-tight"> <span className="text-[11px] font-normal opacity-90 leading-tight">
{offer.priceClass} {offer.priceClass}
@@ -909,7 +931,8 @@ export default function OffersPageClient({
qp.set('outwardTime', '00:00'); qp.set('outwardTime', '00:00');
router.push(`?${qp.toString()}`); router.push(`?${qp.toString()}`);
}} }}
className="bg-[#006837] text-white text-xs font-semibold px-5 py-2.5 rounded-lg hover:bg-[#00522b] cursor-pointer inline-flex items-center gap-2" style={{ backgroundColor: theme.primary }}
className="text-white text-xs font-semibold px-5 py-2.5 rounded-lg hover:opacity-90 cursor-pointer inline-flex items-center gap-2"
> >
<span className="material-icons text-base">schedule</span> <span className="material-icons text-base">schedule</span>
<span>Voir tous les horaires de la journée dès 00:00</span> <span>Voir tous les horaires de la journée dès 00:00</span>
@@ -934,7 +957,8 @@ export default function OffersPageClient({
<button <button
type="button" type="button"
onClick={scrollToTop} onClick={scrollToTop}
className="bg-[#e6f0d7] text-[#006837] text-[12px] font-medium px-4 py-2 rounded-lg flex items-center gap-1 mx-auto hover:bg-[#d8e8c2] cursor-pointer transition-colors shadow-2xs" style={{ backgroundColor: theme.primaryLight, color: theme.primary }}
className="text-[12px] font-medium px-4 py-2 rounded-lg flex items-center gap-1 mx-auto hover:opacity-80 cursor-pointer transition-colors shadow-2xs"
> >
<span>Haut de page</span> <span>Haut de page</span>
<span className="material-icons text-[16px]">north</span> <span className="material-icons text-[16px]">north</span>
@@ -945,7 +969,10 @@ export default function OffersPageClient({
{/* ─── RIGHT SIDEBAR: SEARCH SUMMARY CARD (STRICTLY CONFORME IMG 1 & 2) ─── */} {/* ─── RIGHT SIDEBAR: SEARCH SUMMARY CARD (STRICTLY CONFORME IMG 1 & 2) ─── */}
<div className="w-full lg:w-[310px] flex-shrink-0"> <div className="w-full lg:w-[310px] flex-shrink-0">
<div className="bg-[#eef3da] border border-[#c4d600]/60 rounded-2xl p-5 text-[13.5px] text-gray-800 space-y-2.5 shadow-xs"> <div
className="border rounded-2xl p-5 text-[13.5px] text-gray-800 space-y-2.5 shadow-xs"
style={{ backgroundColor: `${theme.primary}0f`, borderColor: `${theme.secondary || '#c4d600'}80` }}
>
<div className="font-normal text-gray-900"> <div className="font-normal text-gray-900">
Aller le {outwardDate.toLowerCase().includes('lun') ? outwardDate : 'lun. 21 sept.'} à {outwardTime} Aller le {outwardDate.toLowerCase().includes('lun') ? outwardDate : 'lun. 21 sept.'} à {outwardTime}
@@ -1004,7 +1031,10 @@ export default function OffersPageClient({
</h3> </h3>
{/* Table Headers */} {/* Table Headers */}
<div className="flex items-center text-[12px] font-normal text-gray-600 pb-2 border-b border-[#d1d800]"> <div
className="flex items-center text-[12px] font-normal text-gray-600 pb-2 border-b"
style={{ borderColor: theme.secondary || '#d1d800' }}
>
<span className="w-32">Heure de passage</span> <span className="w-32">Heure de passage</span>
<span>Etape</span> <span>Etape</span>
</div> </div>
@@ -1013,8 +1043,11 @@ export default function OffersPageClient({
<div className="py-4 max-h-[60vh] overflow-y-auto space-y-4 relative scrollbar-thin pr-2"> <div className="py-4 max-h-[60vh] overflow-y-auto space-y-4 relative scrollbar-thin pr-2">
{selectedModalOffer.stops && selectedModalOffer.stops.length > 0 ? ( {selectedModalOffer.stops && selectedModalOffer.stops.length > 0 ? (
<div className="relative"> <div className="relative">
{/* Green vertical line connecting all dots */} {/* Vertical line connecting all dots */}
<div className="absolute left-[134px] top-2 bottom-2 w-[2px] bg-[#006837]"></div> <div
className="absolute left-[134px] top-2 bottom-2 w-[2px]"
style={{ backgroundColor: theme.primary }}
></div>
{selectedModalOffer.stops.map((stop, idx) => ( {selectedModalOffer.stops.map((stop, idx) => (
<div key={idx} className="flex items-center text-[14px] font-medium text-gray-900 py-1.5 relative z-10"> <div key={idx} className="flex items-center text-[14px] font-medium text-gray-900 py-1.5 relative z-10">
@@ -1023,9 +1056,12 @@ export default function OffersPageClient({
{stop.time} {stop.time}
</span> </span>
{/* Green Dot */} {/* Dot */}
<div className="w-12 flex justify-center"> <div className="w-12 flex justify-center">
<span className="w-3 h-3 rounded-full bg-[#006837] border-2 border-white shadow-2xs"></span> <span
className="w-3 h-3 rounded-full border-2 border-white shadow-2xs"
style={{ backgroundColor: theme.primary }}
></span>
</div> </div>
{/* Station Name */} {/* Station Name */}
@@ -1046,94 +1082,8 @@ export default function OffersPageClient({
</div> </div>
)} )}
{/* ─── FOOTER SECTION ─── */} {/* ─── FOOTER SECTION (STRICTLY CONFORME IMG 1) ─── */}
<footer className="w-full bg-white border-t border-gray-200 mt-12 pt-6 pb-8 text-gray-700 font-sans"> <SiteFooter regionSlug={regionSlug} />
<div className="max-w-[1280px] mx-auto px-4 sm:px-6 space-y-6">
{/* Region Selector Bar & Logos Row */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Region Dropdown Selector */}
<div className="relative">
<select
value={regionSlug}
onChange={(e) => router.push(`/${e.target.value}/offers`)}
className="bg-white border border-gray-300 rounded-xl px-4 py-2 text-[13px] text-gray-800 font-medium appearance-none pr-8 cursor-pointer hover:border-gray-400 outline-none"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23006837' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 8px center',
backgroundSize: '20px'
}}
>
<option value="bourgogne-franche-comte">Changer de région</option>
<option value="bourgogne-franche-comte">Bourgogne-Franche-Comté</option>
<option value="occitanie">Occitanie</option>
<option value="provence-alpes-cote-azur">Provence-Alpes-Côte d'Azur</option>
<option value="bretagne">Bretagne</option>
<option value="grand-est">Grand Est</option>
<option value="normandie">Normandie</option>
<option value="hauts-de-france">Hauts-de-France</option>
<option value="pays-de-la-loire">Pays de la Loire</option>
<option value="nouvelle-aquitaine">Nouvelle-Aquitaine</option>
<option value="centre-val-de-loire">Centre-Val de Loire</option>
<option value="auvergne-rhone-alpes">Auvergne-Rhône-Alpes</option>
</select>
</div>
{/* Regional & Network Logos */}
<div className="flex items-center gap-6">
<img
src="https://www.ter.sncf.com/assets/medias/bfc/headerLogo.svg"
alt="Logos SNCF et Mobigo"
className="h-9 object-contain"
/>
</div>
</div>
<div className="w-full h-[1px] bg-gray-200"></div>
{/* Social Icons Row */}
<div className="flex justify-center items-center gap-5 text-[#006837]">
<a href="#" className="hover:opacity-75"><span className="material-icons text-[20px]">facebook</span></a>
<a href="#" className="hover:opacity-75"><span className="material-icons text-[20px]">close</span></a>
<a href="#" className="hover:opacity-75"><span className="material-icons text-[20px]">photo_camera</span></a>
<a href="#" className="hover:opacity-75"><span className="material-icons text-[20px]">play_circle</span></a>
<a href="#" className="hover:opacity-75"><span className="material-icons text-[20px]">chat</span></a>
</div>
{/* Legal Links Row */}
<div className="text-center text-[11.5px] text-gray-600 flex flex-wrap justify-center gap-x-3 gap-y-1.5 leading-relaxed">
<a href="#" className="hover:underline">CGU/CGV TRAIN Mobigo</a>
<span>•</span>
<a href="#" className="hover:underline">CGU / CGV Site Internet</a>
<span>•</span>
<a href="#" className="hover:underline">Confidentialité/Cookies</a>
<span>•</span>
<a href="#" className="hover:underline">Gares</a>
<span>•</span>
<a href="#" className="hover:underline">Lignes</a>
<span>•</span>
<a href="#" className="hover:underline">MOBIGO</a>
<span>•</span>
<a href="#" className="hover:underline">Accessibilité : partiellement conforme</a>
<span>•</span>
<a href="#" className="hover:underline">Mentions légales</a>
<span>•</span>
<a href="#" className="hover:underline">Plan du site</a>
<span>•</span>
<a href="#" className="hover:underline">Contacts</a>
</div>
<div className="text-center text-[11.5px] text-gray-600 flex flex-wrap justify-center gap-x-3 leading-relaxed">
<a href="#" className="hover:underline">Demande d'information et réclamations</a>
<span>•</span>
<a href="#" className="hover:underline">Données personnelles</a>
</div>
</div>
</footer>
</div> </div>
); );
File diff suppressed because it is too large. Load diff
+16 -212
View File
@@ -7,7 +7,9 @@ import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme'; import { getRegionTheme } from '@/lib/regionTheme';
import SiteHeader from '@/components/SiteHeader'; import SiteHeader from '@/components/SiteHeader';
import HeaderMenu from '@/components/HeaderMenu'; import HeaderMenu from '@/components/HeaderMenu';
import SiteFooter from '@/components/SiteFooter';
import TimetableSearchForm from '@/components/TimetableSearchForm'; import TimetableSearchForm from '@/components/TimetableSearchForm';
import StationDeparturesBoard from '@/components/StationDeparturesBoard';
interface RegionData { interface RegionData {
nom_site: string; nom_site: string;
@@ -962,6 +964,7 @@ export default function RegionContent({
{/* ─── REUSABLE TIMETABLE SEARCH FORM MODULE (IMAGE 1) ─── */} {/* ─── REUSABLE TIMETABLE SEARCH FORM MODULE (IMAGE 1) ─── */}
<TimetableSearchForm <TimetableSearchForm
regionSlug={regionSlug}
className="w-full lg:w-[410px]" className="w-full lg:w-[410px]"
onSearch={(params) => setSearchSuccessMessage(`Recherche d'horaire lancée de "${params.departure || 'Gare'}" à "${params.arrival || 'Gare'}" pour le ${params.outwardDate}`)} onSearch={(params) => setSearchSuccessMessage(`Recherche d'horaire lancée de "${params.departure || 'Gare'}" à "${params.arrival || 'Gare'}" pour le ${params.outwardDate}`)}
/> />
@@ -1161,139 +1164,17 @@ export default function RegionContent({
<div className="max-w-[1340px] mx-auto px-6"> <div className="max-w-[1340px] mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
{/* ─── COL 1 : PROCHAINS DÉPARTS (TABLE HORAIRES SUPABASE) ─── */} {/* ─── COL 1 : PROCHAINS DÉPARTS (TABLE HORAIRES SUPABASE VIA COMPOSANT RÉUTILISABLE) ─── */}
<div className="bg-white rounded-xl border border-gray-200/90 p-5 shadow-xs flex flex-col justify-between"> <StationDeparturesBoard
<div> regionSlug={regionSlug}
<h3 className="text-[24px] font-light text-[#222222] leading-tight">Prochains départs</h3> initialStation={selectedStation}
<h4 className="text-[18px] font-bold text-[#111111] mb-3.5">en gare de</h4> availableStations={config.stations}
onStationChange={setSelectedStation}
{/* Station pills: 4 grandes gares + bouton "Autre gare" */} onAddToCart={(item) => {
<div className="flex items-center gap-1.5 mb-3.5 overflow-x-auto pb-1 scrollbar-hide"> setCartItems(prev => [...prev, item]);
{config.stations.slice(0, 4).map((station) => { setShowCartDrawer(true);
const isSelected = selectedStation === station; }}
return ( />
<button
key={station}
type="button"
onClick={() => setSelectedStation(station)}
className={`px-3 py-1.5 rounded-full text-[12px] whitespace-nowrap transition-colors cursor-pointer ${
isSelected
? "text-white font-semibold underline underline-offset-3 decoration-white decoration-1 shadow-xs"
: "bg-[#e8f1f5] text-[#222222] hover:bg-[#dbe7ed] font-normal"
}`}
style={isSelected ? {
backgroundColor: 'var(--region-primary, #0066a4)'
} : undefined}
>
{station}
</button>
);
})}
{/* Bouton "Autre gare" */}
<button
type="button"
onClick={() => {
alert("Recherche d'autres gares : cette fonctionnalité sera prochainement disponible.");
}}
className="px-3 py-1.5 rounded-full text-[12px] whitespace-nowrap bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors cursor-pointer flex items-center gap-0.5 border border-gray-200 shrink-0"
>
<span>Autre gare</span>
<span className="material-icons text-[14px]">chevron_right</span>
</button>
</div>
{/* Table header */}
<div className="grid grid-cols-[55px_1fr_32px_40px] text-[11px] font-normal text-[#555555] pb-1 border-b-[1.5px] border-[#f5b800]">
<span>Départ</span>
<span>Destination</span>
<span className="text-center">Voie</span>
<span></span>
</div>
{/* Departure rows — Exclusivement BDD table "horaires" avec prise en compte de jours_personnalises et gares_desservies (quais réels) */}
<div className={`divide-y divide-gray-100 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent ${
expandedDepartures ? 'max-h-[500px]' : 'max-h-[265px] min-h-[160px]'
}`}>
{stationDepartures.length > 0 ? (
stationDepartures.map((dep, dIdx) => {
return (
<div key={dep.id || dIdx} className="grid grid-cols-[55px_1fr_32px_40px] items-center py-2.5 hover:bg-gray-50/70 transition-colors rounded-sm px-0.5">
{/* Time */}
<div className="flex flex-col">
<span className="text-[13.5px] font-semibold text-[#111111]">
{dep.scheduledTime}
</span>
</div>
{/* Destination */}
<div className="pr-1">
<div className="font-semibold text-[13.5px] text-[#111111] leading-tight line-clamp-1">
{dep.destName}
</div>
<div className="flex items-center text-[11px] text-[#666666] mt-0.5">
<span
className="material-icons text-[13px] mr-1"
style={{ color: 'var(--region-primary, #0066a4)' }}
>
train
</span>
<span>{dep.trainLabel}</span>
</div>
</div>
{/* Voie / Quai réel de l'horaire pour cette gare */}
<span className={`text-center font-medium text-[13px] ${
dep.track !== '-' ? 'text-[#0066a4] font-bold bg-[#e8f1f5] rounded-xs px-1 py-0.5' : 'text-[#666666]'
}`}>
{dep.track}
</span>
{/* Buy Button */}
<button
type="button"
onClick={() => {
setCartItems(prev => [...prev, {
id: Date.now(),
title: `Billet ${selectedStation} → ${dep.destName}`,
price: 12.00,
date: new Date().toLocaleDateString('fr-FR')
}]);
setShowCartDrawer(true);
}}
className="bg-[#e8f1f5] hover:bg-[#dbe7ed] px-2 py-0.5 rounded-full flex items-center justify-center gap-0.5 cursor-pointer transition-colors ml-auto shadow-2xs"
style={{ color: 'var(--region-primary, #0066a4)' }}
title={`Acheter un billet pour ${dep.destName}`}
>
<span className="material-icons text-[14px]">shopping_basket</span>
<span className="font-bold text-[10px]">›</span>
</button>
</div>
);
})
) : (
<div className="py-8 text-center flex flex-col items-center justify-center">
<span className="material-icons text-[32px] text-gray-300 mb-1.5">schedule</span>
<p className="text-[13px] font-medium text-gray-700 mb-0.5">
Aucun départ programmé en gare de {selectedStation}
</p>
<p className="text-[11px] text-gray-500 max-w-xs">
Les horaires de cette gare seront enregistrés prochainement dans la base de données.
</p>
</div>
)}
</div>
</div>
{/* Voir plus d'horaires link */}
<div
onClick={() => setExpandedDepartures(!expandedDepartures)}
className="flex justify-end text-[13px] font-normal text-[#222222] mt-3.5 cursor-pointer hover:underline items-center gap-1"
>
<span>Voir plus d&apos;horaires</span>
<span className="material-icons text-[16px]">chevron_right</span>
</div>
</div>
{/* ─── COL 2 & 3 : LES 2 DERNIÈRES ACTUALITÉS MISES EN AVANT (BDD SUPABASE) ─── */} {/* ─── COL 2 & 3 : LES 2 DERNIÈRES ACTUALITÉS MISES EN AVANT (BDD SUPABASE) ─── */}
{top2Featured.map((actu, idx) => ( {top2Featured.map((actu, idx) => (
@@ -1571,85 +1452,8 @@ export default function RegionContent({
{/* 7. FOOTER */} {/* 7. FOOTER */}
{/* ================================================================== */} {/* ================================================================== */}
{/* Haut de page */} {/* REUSABLE SITE FOOTER (Image 1) */}
<div className="bg-white text-center py-4 border-t border-gray-100"> <SiteFooter regionSlug={regionSlug} />
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="inline-flex items-center gap-1 font-semibold text-[11px] px-4 py-1.5 rounded-full shadow-2xs transition-opacity hover:opacity-90 cursor-pointer"
style={{
backgroundColor: 'var(--region-primary-light, #e8f1f5)',
color: 'var(--region-primary, #0066a4)'
}}
>
Haut de page
<span className="material-icons text-[14px]">arrow_upward</span>
</button>
</div>
<footer className="bg-[#f2f4f5] pt-6 pb-8 border-t border-gray-200">
<div className="max-w-[1340px] mx-auto px-6">
{/* Region selector */}
<div className="mb-5">
<select
value={regionSlug}
onChange={handleRegionChange}
className="bg-white text-gray-800 font-medium text-[12px] px-3.5 py-2 rounded-md shadow-2xs border border-gray-200 cursor-pointer outline-none"
>
<option value="sud-provence-alpes-cote-d-azur">Sud Provence-Alpes-Côte d'Azur</option>
<option value="bourgogne-franche-comte">Bourgogne-Franche-Comté</option>
<option value="auvergne-rhone-alpes">Auvergne-Rhône-Alpes</option>
<option value="bretagne">Bretagne</option>
<option value="centre-val-de-loire">Centre-Val de Loire</option>
<option value="grand-est">Grand Est</option>
<option value="hauts-de-france">Hauts-de-France</option>
<option value="normandie">Normandie</option>
<option value="nouvelle-aquitaine">Nouvelle-Aquitaine</option>
<option value="occitanie">Occitanie</option>
<option value="pays-de-la-loire">Pays de la Loire</option>
</select>
</div>
{/* Regional accent line */}
<div
className="border-t-2 mb-5"
style={{ borderColor: 'var(--region-primary, #0066a4)' }}
></div>
{/* Social icons */}
<div
className="flex justify-center gap-6 mb-5"
style={{ color: 'var(--region-primary, #0066a4)' }}
>
<span className="material-icons text-[22px] cursor-pointer hover:opacity-80">facebook</span>
<span className="font-extrabold text-[18px] cursor-pointer hover:opacity-80 leading-none pt-0.5">𝕏</span>
<span className="material-icons text-[22px] cursor-pointer hover:opacity-80">camera_alt</span>
<span className="material-icons text-[22px] cursor-pointer hover:opacity-80">play_circle_filled</span>
<span className="material-icons text-[22px] cursor-pointer hover:opacity-80">chat</span>
</div>
{/* Legal links */}
<div className="text-center text-[11px] text-gray-500 leading-normal max-w-4xl mx-auto">
<div className="flex flex-wrap justify-center gap-x-2 gap-y-1 mb-1.5">
{["CGU/CGV Billets Régionaux", "CGU / CGV Site Internet", "Confidentialité/Cookies", "Gares", "Lignes", "ZOU !", "Accessibilité : partiellement conforme", "Mentions légales"].map((link, i, arr) => (
<React.Fragment key={link}>
<a href="#" className="hover:text-black hover:underline">{link}</a>
{i < arr.length - 1 && <span className="opacity-40">•</span>}
</React.Fragment>
))}
</div>
<div className="flex flex-wrap justify-center gap-x-2">
{["Plan du site", "Contacts", "Demande d'information et réclamations", "Données personnelles"].map((link, i, arr) => (
<React.Fragment key={link}>
<a href="#" className="hover:text-black hover:underline">{link}</a>
{i < arr.length - 1 && <span className="opacity-40">•</span>}
</React.Fragment>
))}
</div>
</div>
</div>
</footer>
{/* ================================================================== */} {/* ================================================================== */}
{/* 8. MODALES & TIROIRS */} {/* 8. MODALES & TIROIRS */}
+260
View File
@@ -0,0 +1,260 @@
"use client";
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { getRegionTheme } from '@/lib/regionTheme';
interface SiteFooterProps {
regionSlug?: string;
showScrollToTop?: boolean;
}
const REGIONS_LIST = [
{ slug: 'bourgogne-franche-comte', name: 'Bourgogne-Franche-Comté' },
{ slug: 'auvergne-rhone-alpes', name: 'Auvergne-Rhône-Alpes' },
{ slug: 'grand-est', name: 'Grand Est' },
{ slug: 'hauts-de-france', name: 'Hauts-de-France' },
{ slug: 'normandie', name: 'Normandie' },
{ slug: 'nouvelle-aquitaine', name: 'Nouvelle-Aquitaine' },
{ slug: 'occitanie', name: 'Occitanie' },
{ slug: 'pays-de-la-loire', name: 'Pays de la Loire' },
{ slug: 'centre-val-de-loire', name: 'Centre-Val de Loire' },
{ slug: 'bretagne', name: 'Bretagne' },
{ slug: 'sud-provence-alpes-cote-d-azur', name: "Provence-Alpes-Côte d'Azur" },
];
export default function SiteFooter({
regionSlug = 'bourgogne-franche-comte',
showScrollToTop = true,
}: SiteFooterProps) {
const router = useRouter();
const [selectedRegion, setSelectedRegion] = useState(regionSlug);
const theme = getRegionTheme(selectedRegion || regionSlug);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleRegionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newSlug = e.target.value;
setSelectedRegion(newSlug);
router.push(`/${newSlug}`);
};
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
return (
<footer className="w-full bg-[#fcfdfa] border-t border-gray-200 mt-12 text-[#222]">
{/* ─── 1. HAUT DE PAGE BUTTON (Centered at top) ─── */}
{showScrollToTop && (
<div className="flex justify-center -mt-3.5 mb-4">
<button
type="button"
onClick={scrollToTop}
className="px-3.5 py-1 rounded text-[12px] font-medium inline-flex items-center gap-1 shadow-2xs cursor-pointer transition-colors border"
style={{
backgroundColor: theme.primaryLight,
color: theme.primary,
borderColor: theme.secondary || theme.primary
}}
>
<span>Haut de page</span>
<span className="material-icons text-[14px]">arrow_upward</span>
</button>
</div>
)}
<div className="max-w-[1240px] mx-auto px-4 sm:px-6 pt-3 pb-8">
{/* ─── 2. TOP ROW: CHANGER DE RÉGION + LOGOS PARTENAIRES ─── */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 pb-6 border-b border-gray-200">
{/* Region Switcher Select */}
<div className="relative">
<select
value={selectedRegion}
onChange={handleRegionChange}
className="appearance-none bg-white border border-gray-300 hover:border-gray-400 text-gray-800 text-[13px] font-normal py-2 pl-3.5 pr-9 rounded-md shadow-2xs focus:outline-none cursor-pointer min-w-[180px]"
>
<option value="" disabled>Changer de région</option>
{REGIONS_LIST.map((r) => (
<option key={r.slug} value={r.slug}>
{r.name}
</option>
))}
</select>
<span className="material-icons absolute right-2.5 top-2.5 text-[18px] pointer-events-none" style={{ color: theme.primary }}>
expand_more
</span>
</div>
{/* Regional & Operator Partner Logos */}
<div className="flex items-center gap-5 sm:gap-6 flex-wrap justify-center">
{/* SNCF Voyageurs */}
<div className="flex items-center gap-1">
<span className="text-[10px] uppercase font-bold text-gray-400 tracking-wider">Opéré par</span>
<span className="bg-[#e01a22] text-white font-extrabold text-[11px] px-1.5 py-0.5 rounded tracking-tighter">
SNCF
</span>
<span className="text-[10px] font-semibold text-gray-700">Voyageurs</span>
</div>
{/* Region Logo badge */}
<div className="border-l border-gray-300 pl-4 flex items-center gap-1.5 text-[11px] font-semibold text-gray-700">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: theme.primary }} />
<span>RÉGION {theme.regionName.toUpperCase()}</span>
</div>
{/* Regional Brand */}
<div className="border-l border-gray-300 pl-4 flex items-center">
<span className="font-black text-[15px] tracking-tight" style={{ color: theme.primary }}>
{theme.operatorLabel}
</span>
</div>
</div>
</div>
{/* ─── 3. SOCIAL MEDIA ICONS (Centered) ─── */}
<div className="flex items-center justify-center gap-5 py-6">
{/* Facebook */}
<a
href="https://facebook.com"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
style={{ color: theme.primary }}
aria-label="Facebook"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
{/* X (Twitter) */}
<a
href="https://twitter.com"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
style={{ color: theme.primary }}
aria-label="X (Twitter)"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
{/* Instagram */}
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
style={{ color: theme.primary }}
aria-label="Instagram"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
{/* YouTube */}
<a
href="https://youtube.com"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
style={{ color: theme.primary }}
aria-label="YouTube"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
{/* WhatsApp / Message */}
<a
href="https://whatsapp.com"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 rounded-full flex items-center justify-center transition-colors"
style={{ color: theme.primary }}
aria-label="WhatsApp"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/>
</svg>
</a>
</div>
{/* ─── 4. LEGAL & NAVIGATION LINKS (Image 1) ─── */}
<div className="text-center text-[12px] sm:text-[12.5px] text-gray-600 space-y-2 pt-2 border-t border-gray-100">
{/* First Line */}
<div className="flex flex-wrap justify-center items-center gap-x-3 gap-y-1.5 font-normal">
<Link href={getRegionPath('/cgu')} className="hover:underline hover:text-black">
CGU/CGV TRAIN {theme.operatorLabel}
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/cgu-site')} className="hover:underline hover:text-black">
CGU / CGV Site Internet
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/confidentialite')} className="hover:underline hover:text-black">
Confidentialité/Cookies
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/se-deplacer/gares')} className="hover:underline hover:text-black">
Gares
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/se-deplacer/fiches-horaires')} className="hover:underline hover:text-black">
Lignes
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/#brand')} className="hover:underline hover:text-black">
{theme.operatorLabel}
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/accessibilite')} className="hover:underline hover:text-black">
Accessibilité : partiellement conforme
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/mentions-legales')} className="hover:underline hover:text-black">
Mentions légales
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/plan-du-site')} className="hover:underline hover:text-black">
Plan du site
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/contacts')} className="hover:underline hover:text-black">
Contacts
</Link>
</div>
{/* Second Line */}
<div className="flex flex-wrap justify-center items-center gap-x-3 gap-y-1 font-normal text-gray-500">
<Link href={getRegionPath('/reclamations')} className="hover:underline hover:text-black">
Demande d&apos;information et réclamations
</Link>
<span className="text-gray-300">•</span>
<Link href={getRegionPath('/donnees-personnelles')} className="hover:underline hover:text-black">
Données personnelles
</Link>
</div>
<div className="pt-3 text-[11px] text-gray-400">
© FERROVIA - Tous droits réservés.
</div>
</div>
</div>
</footer>
);
}
+344
View File
@@ -0,0 +1,344 @@
"use client";
import React, { useState, useEffect, useMemo } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
export interface ResolvedDeparture {
id: string | number;
scheduledTime: string;
destName: string;
trainType: string;
trainNumber: string;
trainLabel: string;
track: string;
rawQuai?: string | null;
sourceHoraire?: any;
}
interface StationDeparturesBoardProps {
regionSlug?: string;
initialStation?: string;
showStationSelector?: boolean;
availableStations?: string[];
onStationChange?: (station: string) => void;
onAddToCart?: (item: { id: number; title: string; price: number; date: string }) => void;
className?: string;
}
export default function StationDeparturesBoard({
regionSlug = "bourgogne-franche-comte",
initialStation = "Dijon",
showStationSelector = true,
availableStations,
onStationChange,
onAddToCart,
className = "",
}: StationDeparturesBoardProps) {
const theme = getRegionTheme(regionSlug);
const [selectedStation, setSelectedStation] = useState<string>(initialStation);
const [horairesDataList, setHorairesDataList] = useState<any[]>([]);
const [garesTypeMap, setGaresTypeMap] = useState<Record<string, string>>({});
const [expandedDepartures, setExpandedDepartures] = useState<boolean>(false);
// Sync station if prop changes
useEffect(() => {
if (initialStation) {
setSelectedStation(initialStation);
}
}, [initialStation]);
// Fetch gares and horaires
useEffect(() => {
const fetchBoardData = async () => {
try {
const { data: gData } = await supabase
.from('gares')
.select('id, nom, type')
.is('deleted_at', null);
const gMap: Record<string, string> = {};
const gTypeMap: Record<string, string> = {};
if (gData) {
gData.forEach((g: any) => {
gMap[g.id] = g.nom;
gTypeMap[g.nom.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim()] = g.type || 'ville';
});
setGaresTypeMap(gTypeMap);
}
const { data: hData } = await supabase
.from('horaires')
.select('*')
.eq('actif', true)
.is('deleted_at', null)
.order('heure_depart', { ascending: true });
if (hData) {
setHorairesDataList(hData.map((h: any) => ({
...h,
gare_depart_nom: gMap[h.gare_depart_id] || '',
gare_arrivee_nom: gMap[h.gare_arrivee_id] || '',
})));
}
} catch (err) {
console.error('Error fetching data for StationDeparturesBoard:', err);
}
};
fetchBoardData();
}, [regionSlug]);
const normalizeStationName = (s: string) => (s || '').toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
const getLocalDateStr = (d: Date = new Date()) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const isTrainCirculating = (h: any, targetDate: string = getLocalDateStr()) => {
if (h.actif === false) return false;
if (Array.isArray(h.jours_personnalises) && h.jours_personnalises.length > 0) {
const customDay = h.jours_personnalises.find((jp: any) => {
const d = jp.date || jp.jour || '';
return d === targetDate || d.slice(0, 10) === targetDate;
});
if (customDay) return customDay.circule === true;
}
return false;
};
// Filter and resolve departures for selectedStation
const stationDepartures: ResolvedDeparture[] = useMemo(() => {
if (!selectedStation || horairesDataList.length === 0) return [];
const selectedStationNorm = normalizeStationName(selectedStation);
const todayDateStr = getLocalDateStr();
const results: ResolvedDeparture[] = [];
for (const h of horairesDataList) {
if (!isTrainCirculating(h, todayDateStr)) continue;
let gares: any[] = [];
if (typeof h.gares_desservies === 'string') {
try { gares = JSON.parse(h.gares_desservies); } catch (e) { gares = []; }
} else if (Array.isArray(h.gares_desservies)) {
gares = h.gares_desservies;
}
const stopIndex = gares.findIndex(g => {
const gNomNorm = normalizeStationName(g.gare_nom || '');
return gNomNorm === selectedStationNorm ||
gNomNorm.includes(selectedStationNorm) ||
selectedStationNorm.includes(gNomNorm);
});
if (stopIndex !== -1) {
const stop = gares[stopIndex];
const isTerminus = stop.is_arrivee || (stopIndex === gares.length - 1 && !stop.heure_depart) || !stop.heure_depart;
if (!isTerminus) {
const lastStop = gares[gares.length - 1];
const destination = h.gare_arrivee_nom || lastStop?.gare_nom || "Terminus";
const departureTime = (stop.heure_depart || '').slice(0, 5);
// Track display logic
const currentStationType = garesTypeMap[selectedStationNorm] || 'ville';
const now = new Date();
const [depH, depM] = departureTime.split(':').map(Number);
const depMinutes = (isNaN(depH) ? 0 : depH) * 60 + (isNaN(depM) ? 0 : depM);
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const diffMinutes = depMinutes - currentMinutes;
let displayQuai = '-';
if (stop.quai && stop.quai.trim() !== '') {
if (currentStationType === 'ville') {
if (diffMinutes <= 30) displayQuai = stop.quai;
} else {
if (diffMinutes <= 360) displayQuai = stop.quai;
}
}
if (displayQuai === '-' && stop.quai) {
displayQuai = stop.quai;
}
results.push({
id: `${h.id}-${stopIndex}`,
scheduledTime: departureTime,
destName: destination,
trainType: h.type_train || 'TRAIN MOBIGO',
trainNumber: h.numero_train || '',
trainLabel: `${h.type_train?.toUpperCase() || theme.operatorLabel || 'TER'} ${h.numero_train || ''}`.trim(),
track: displayQuai,
rawQuai: stop.quai,
sourceHoraire: h,
});
}
}
}
return results.sort((a, b) => a.scheduledTime.localeCompare(b.scheduledTime));
}, [selectedStation, horairesDataList, garesTypeMap, theme.operatorLabel]);
const defaultPillStations = availableStations || [
"Dijon", "Besançon Viotte", "Belfort", "Chalon-sur-Saône"
];
const handleSelectStation = (st: string) => {
setSelectedStation(st);
if (onStationChange) onStationChange(st);
};
const getRegionPath = (path: string) => {
return regionSlug ? `/${regionSlug}${path}` : path;
};
return (
<div className={`bg-white rounded-xl border border-gray-200/90 p-5 shadow-xs flex flex-col justify-between ${className}`}>
<div>
<h3 className="text-[24px] font-light text-[#222222] leading-tight">Prochains départs</h3>
<h4 className="text-[18px] font-bold text-[#111111] mb-3.5">en gare de</h4>
{/* Station pills (if selector enabled) */}
{showStationSelector && (
<div className="flex items-center gap-1.5 mb-3.5 overflow-x-auto pb-1 scrollbar-hide">
{defaultPillStations.slice(0, 4).map((st) => {
const isSelected = selectedStation.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim() === st.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
return (
<button
key={st}
type="button"
onClick={() => handleSelectStation(st)}
className={`px-3 py-1.5 rounded-full text-[12px] whitespace-nowrap transition-colors cursor-pointer ${
isSelected
? "text-white font-semibold underline underline-offset-3 decoration-white decoration-1 shadow-xs"
: "bg-[#e8f1f5] text-[#222222] hover:bg-[#dbe7ed] font-normal"
}`}
style={isSelected ? { backgroundColor: theme.primary } : undefined}
>
{st}
</button>
);
})}
<Link
href={getRegionPath('/se-deplacer/prochains-departs')}
className="px-3 py-1.5 rounded-full text-[12px] whitespace-nowrap bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors cursor-pointer flex items-center gap-0.5 border border-gray-200 shrink-0"
>
<span>Autre gare</span>
<span className="material-icons text-[14px]">chevron_right</span>
</Link>
</div>
)}
{/* Table header */}
<div
className="grid grid-cols-[55px_1fr_32px_40px] text-[11px] font-normal text-[#555555] pb-1 border-b-[1.5px]"
style={{ borderBottomColor: theme.secondary || '#f5b800' }}
>
<span>Départ</span>
<span>Destination</span>
<span className="text-center">Voie</span>
<span></span>
</div>
{/* Departure rows */}
<div className={`divide-y divide-gray-100 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent ${
expandedDepartures ? 'max-h-[500px]' : 'max-h-[265px] min-h-[160px]'
}`}>
{stationDepartures.length > 0 ? (
stationDepartures.map((dep, dIdx) => (
<div key={dep.id || dIdx} className="grid grid-cols-[55px_1fr_32px_40px] items-center py-2.5 hover:bg-gray-50/70 transition-colors rounded-sm px-0.5">
{/* Time */}
<div className="flex flex-col">
<span className="text-[13.5px] font-semibold text-[#111111]">
{dep.scheduledTime}
</span>
</div>
{/* Destination */}
<div className="pr-1">
<div className="font-semibold text-[13.5px] text-[#111111] leading-tight line-clamp-1">
{dep.destName}
</div>
<div className="flex items-center text-[11px] text-[#666666] mt-0.5">
<span
className="material-icons text-[13px] mr-1"
style={{ color: theme.primary }}
>
train
</span>
<span className="truncate">{dep.trainLabel}</span>
</div>
</div>
{/* Voie / Quai */}
<span className={`text-center font-medium text-[13px] ${
dep.track !== '-' ? 'font-bold rounded-xs px-1 py-0.5' : 'text-[#666666]'
}`}
style={dep.track !== '-' ? { backgroundColor: theme.primaryLight, color: theme.primary } : undefined}
>
{dep.track}
</span>
{/* Buy / Action Button */}
<button
type="button"
onClick={() => {
if (onAddToCart) {
onAddToCart({
id: Date.now() + dIdx,
title: `Billet ${selectedStation} → ${dep.destName}`,
price: 12.00,
date: new Date().toLocaleDateString('fr-FR')
});
}
}}
className="px-2 py-0.5 rounded-full flex items-center justify-center gap-0.5 cursor-pointer transition-colors ml-auto shadow-2xs hover:opacity-90"
style={{ backgroundColor: theme.primaryLight, color: theme.primary }}
title={`Acheter un billet pour ${dep.destName}`}
>
<span className="material-icons text-[14px]">shopping_basket</span>
<span className="font-bold text-[10px]">›</span>
</button>
</div>
))
) : (
<div className="py-8 text-center flex flex-col items-center justify-center">
<span className="material-icons text-[32px] text-gray-300 mb-1.5">schedule</span>
<p className="text-[13px] font-medium text-gray-700 mb-0.5">
Aucun départ programmé en gare de {selectedStation}
</p>
<p className="text-[11px] text-gray-500 max-w-xs">
Les horaires de cette gare seront enregistrés prochainement dans la base de données.
</p>
</div>
)}
</div>
</div>
{/* Voir plus d'horaires link */}
<div className="flex justify-between items-center text-[13px] font-normal text-[#222222] mt-3.5 pt-2 border-t border-gray-100">
<button
type="button"
onClick={() => setExpandedDepartures(!expandedDepartures)}
className="text-gray-500 hover:text-black cursor-pointer text-[12px]"
>
{expandedDepartures ? "Réduire" : "Déplier la liste"}
</button>
<Link
href={getRegionPath(`/se-deplacer/prochains-departs?gare=${encodeURIComponent(selectedStation)}`)}
className="hover:underline flex items-center gap-0.5 font-medium"
style={{ color: theme.primary }}
>
<span>Voir plus d&apos;horaires</span>
<span className="material-icons text-[16px]">chevron_right</span>
</Link>
</div>
</div>
);
}
+158 -32
View File
@@ -1,8 +1,9 @@
"use client"; "use client";
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { getRegionTheme } from '@/lib/regionTheme';
type ActiveField = 'departure' | 'arrival' | 'via' | 'outwardDate' | 'outwardTime' | 'returnDate' | 'returnTime' | 'passenger' | null; type ActiveField = 'departure' | 'arrival' | 'via' | 'outwardDate' | 'outwardTime' | 'returnDate' | 'returnTime' | 'passenger' | null;
@@ -87,6 +88,7 @@ export default function TimetableSearchForm({
}: TimetableSearchFormProps) { }: TimetableSearchFormProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// Determine current region slug from props or URL pathname // Determine current region slug from props or URL pathname
@@ -100,6 +102,8 @@ export default function TimetableSearchForm({
return ''; return '';
})(); })();
const theme = getRegionTheme(currentRegionSlug);
// Form states // Form states
const [departureStation, setDepartureStation] = useState(initialDeparture); const [departureStation, setDepartureStation] = useState(initialDeparture);
const [arrivalStation, setArrivalStation] = useState(initialArrival); const [arrivalStation, setArrivalStation] = useState(initialArrival);
@@ -126,6 +130,76 @@ export default function TimetableSearchForm({
{ id: 1, age: '', discountCard: 'Carte de réduction (optionnel)' } { id: 1, age: '', discountCard: 'Carte de réduction (optionnel)' }
]); ]);
// Priority: URL searchParams > props > sessionStorage > default
useEffect(() => {
// 1. Try from URL search params
const paramDep = searchParams?.get('departure');
const paramArr = searchParams?.get('arrival');
const paramVia = searchParams?.get('via');
const paramOutDate = searchParams?.get('outwardDate');
const paramOutTime = searchParams?.get('outwardTime');
const paramHasReturn = searchParams?.get('hasReturn');
const paramRetDate = searchParams?.get('returnDate');
const paramRetTime = searchParams?.get('returnTime');
const paramPassCount = searchParams?.get('passengers');
const paramPassAge = searchParams?.get('passengerAge');
const paramDiscount = searchParams?.get('discountCard');
if (paramDep || paramArr || paramOutDate) {
if (paramDep) setDepartureStation(paramDep);
if (paramArr) setArrivalStation(paramArr);
if (paramVia) {
setViaStation(paramVia);
setShowVia(true);
}
if (paramOutDate) setOutwardDate(paramOutDate);
if (paramOutTime) setOutwardTime(paramOutTime);
if (paramHasReturn === '1' || (paramRetDate && paramRetDate !== '')) {
setHasReturn(true);
if (paramRetDate) setReturnDate(paramRetDate);
if (paramRetTime) setReturnTime(paramRetTime);
}
const count = parseInt(paramPassCount || '1', 10);
const newPassengers: PassengerInfo[] = [];
const total = isNaN(count) || count < 1 ? 1 : count;
for (let i = 0; i < total; i++) {
newPassengers.push({
id: i + 1,
age: i === 0 ? (paramPassAge || '') : '',
discountCard: i === 0 ? (paramDiscount || 'Carte de réduction (optionnel)') : 'Carte de réduction (optionnel)',
});
}
setPassengers(newPassengers);
return;
}
// 2. Try from sessionStorage if available
try {
const stored = sessionStorage.getItem('ferrovia_last_search');
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.departure) setDepartureStation(parsed.departure);
if (parsed.arrival) setArrivalStation(parsed.arrival);
if (parsed.via) {
setViaStation(parsed.via);
setShowVia(true);
}
if (parsed.outwardDate) setOutwardDate(parsed.outwardDate);
if (parsed.outwardTime) setOutwardTime(parsed.outwardTime);
if (parsed.hasReturn) {
setHasReturn(true);
if (parsed.returnDate) setReturnDate(parsed.returnDate);
if (parsed.returnTime) setReturnTime(parsed.returnTime);
}
if (parsed.passengers && Array.isArray(parsed.passengers) && parsed.passengers.length > 0) {
setPassengers(parsed.passengers);
}
}
} catch (e) {
// ignore
}
}, [searchParams]);
// Fetch stations from Supabase // Fetch stations from Supabase
useEffect(() => { useEffect(() => {
const fetchGares = async () => { const fetchGares = async () => {
@@ -186,6 +260,26 @@ export default function TimetableSearchForm({
? firstPassenger.discountCard ? firstPassenger.discountCard
: 'Sans carte'; : 'Sans carte';
const searchData = {
departure: departureStation || 'Dijon',
arrival: arrivalStation || 'Seurre',
via: showVia ? viaStation : '',
outwardDate,
outwardTime,
hasReturn,
returnDate: hasReturn ? returnDate : '',
returnTime: hasReturn ? returnTime : '',
passengers,
passengerCount: passengers.length,
discountCard: discountStr,
};
try {
sessionStorage.setItem('ferrovia_last_search', JSON.stringify(searchData));
} catch (err) {
// ignore
}
if (onSearch) { if (onSearch) {
onSearch({ onSearch({
departure: departureStation || 'Dijon', departure: departureStation || 'Dijon',
@@ -311,14 +405,15 @@ export default function TimetableSearchForm({
{/* 1. Gare de départ */} {/* 1. Gare de départ */}
<div <div
onClick={() => setActiveField('departure')} onClick={() => setActiveField('departure')}
style={activeField === 'departure' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${ className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${
activeField === 'departure' activeField === 'departure'
? 'bg-[#fff8f8] border-2 border-dashed border-[#c00] shadow-xs' ? 'border-2 border-dashed shadow-xs'
: 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]'
}`} }`}
> >
<div className="flex items-center gap-3 flex-1"> <div className="flex items-center gap-3 flex-1">
<span className="material-icons text-[22px] text-[#006837] flex-shrink-0"> <span className="material-icons text-[22px] flex-shrink-0" style={{ color: theme.primary }}>
place place
</span> </span>
<input <input
@@ -336,7 +431,8 @@ export default function TimetableSearchForm({
<button <button
type="button" type="button"
onClick={handleSwapStations} onClick={handleSwapStations}
className="hover:opacity-80 transition-transform active:rotate-180 duration-200 pl-2 cursor-pointer text-[#006837]" className="hover:opacity-80 transition-transform active:rotate-180 duration-200 pl-2 cursor-pointer"
style={{ color: theme.primary }}
title="Inverser les gares" title="Inverser les gares"
> >
<span className="material-icons text-[20px]">swap_vert</span> <span className="material-icons text-[20px]">swap_vert</span>
@@ -346,13 +442,14 @@ export default function TimetableSearchForm({
{/* 2. Gare d'arrivée */} {/* 2. Gare d'arrivée */}
<div <div
onClick={() => setActiveField('arrival')} onClick={() => setActiveField('arrival')}
style={activeField === 'arrival' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-3 flex items-center gap-3 transition-all cursor-pointer ${ className={`rounded-xl px-4 py-3 flex items-center gap-3 transition-all cursor-pointer ${
activeField === 'arrival' activeField === 'arrival'
? 'bg-[#fff8f8] border-2 border-dashed border-[#c00] shadow-xs' ? 'border-2 border-dashed shadow-xs'
: 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]'
}`} }`}
> >
<span className="material-icons text-[22px] text-[#006837] flex-shrink-0"> <span className="material-icons text-[22px] flex-shrink-0" style={{ color: theme.primary }}>
place place
</span> </span>
<input <input
@@ -376,7 +473,8 @@ export default function TimetableSearchForm({
setShowVia(!showVia); setShowVia(!showVia);
if (!showVia) setActiveField('via'); if (!showVia) setActiveField('via');
}} }}
className="text-[12px] font-medium text-[#006837] cursor-pointer hover:underline" className="text-[12px] font-medium cursor-pointer hover:underline"
style={{ color: theme.primary }}
> >
{showVia ? "- Supprimer Via" : "+ Via"} {showVia ? "- Supprimer Via" : "+ Via"}
</button> </button>
@@ -385,9 +483,10 @@ export default function TimetableSearchForm({
{showVia && ( {showVia && (
<div <div
onClick={() => setActiveField('via')} onClick={() => setActiveField('via')}
style={activeField === 'via' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-2.5 flex items-center gap-2.5 transition-all cursor-pointer ${ className={`rounded-xl px-4 py-2.5 flex items-center gap-2.5 transition-all cursor-pointer ${
activeField === 'via' activeField === 'via'
? 'bg-[#fff8f8] border-2 border-dashed border-[#c00]' ? 'border-2 border-dashed'
: 'bg-[#f2f3f5] hover:bg-[#eaecef]' : 'bg-[#f2f3f5] hover:bg-[#eaecef]'
}`} }`}
> >
@@ -405,9 +504,10 @@ export default function TimetableSearchForm({
{/* 3. Aller */} {/* 3. Aller */}
<div <div
style={activeField === 'outwardDate' || activeField === 'outwardTime' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-2.5 flex items-center justify-between transition-all ${ className={`rounded-xl px-4 py-2.5 flex items-center justify-between transition-all ${
activeField === 'outwardDate' || activeField === 'outwardTime' activeField === 'outwardDate' || activeField === 'outwardTime'
? 'bg-[#f7faef] border-2 border-dashed border-[#006837] shadow-xs' ? 'border-2 border-dashed shadow-xs'
: 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]'
}`} }`}
> >
@@ -415,7 +515,7 @@ export default function TimetableSearchForm({
onClick={() => setActiveField('outwardDate')} onClick={() => setActiveField('outwardDate')}
className="flex items-center gap-3 flex-1 cursor-pointer" className="flex items-center gap-3 flex-1 cursor-pointer"
> >
<span className="material-icons text-[20px] text-[#006837] flex-shrink-0"> <span className="material-icons text-[20px] flex-shrink-0" style={{ color: theme.primary }}>
event event
</span> </span>
<div className="flex flex-col"> <div className="flex flex-col">
@@ -430,7 +530,7 @@ export default function TimetableSearchForm({
<div <div
onClick={() => setActiveField('outwardTime')} onClick={() => setActiveField('outwardTime')}
className="w-14 text-left font-normal text-[14px] text-gray-900 cursor-pointer hover:text-[#006837]" className="w-14 text-left font-normal text-[14px] text-gray-900 cursor-pointer hover:opacity-80"
> >
{outwardTime} {outwardTime}
</div> </div>
@@ -438,9 +538,10 @@ export default function TimetableSearchForm({
{/* 4. Retour */} {/* 4. Retour */}
<div <div
style={activeField === 'returnDate' || activeField === 'returnTime' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all ${ className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all ${
activeField === 'returnDate' || activeField === 'returnTime' activeField === 'returnDate' || activeField === 'returnTime'
? 'bg-[#f7faef] border-2 border-dashed border-[#006837] shadow-xs' ? 'border-2 border-dashed shadow-xs'
: 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]'
}`} }`}
> >
@@ -451,7 +552,7 @@ export default function TimetableSearchForm({
}} }}
className="flex items-center gap-3 flex-1 cursor-pointer" className="flex items-center gap-3 flex-1 cursor-pointer"
> >
<span className="material-icons text-[20px] text-[#006837] flex-shrink-0"> <span className="material-icons text-[20px] flex-shrink-0" style={{ color: theme.primary }}>
event event
</span> </span>
<span className={`text-[13.5px] ${hasReturn ? 'text-gray-900 font-semibold' : 'text-gray-600 font-normal'}`}> <span className={`text-[13.5px] ${hasReturn ? 'text-gray-900 font-semibold' : 'text-gray-600 font-normal'}`}>
@@ -467,7 +568,7 @@ export default function TimetableSearchForm({
setHasReturn(true); setHasReturn(true);
setActiveField('returnTime'); setActiveField('returnTime');
}} }}
className="w-14 text-left font-normal text-[13.5px] text-gray-700 cursor-pointer hover:text-[#006837]" className="w-14 text-left font-normal text-[13.5px] text-gray-700 cursor-pointer hover:opacity-80"
> >
{hasReturn ? returnTime : "--:--"} {hasReturn ? returnTime : "--:--"}
</div> </div>
@@ -496,14 +597,15 @@ export default function TimetableSearchForm({
{/* 5. Voyageurs (CONFORME IMG 1) */} {/* 5. Voyageurs (CONFORME IMG 1) */}
<div <div
onClick={() => setActiveField('passenger')} onClick={() => setActiveField('passenger')}
style={activeField === 'passenger' ? { borderColor: theme.primary, backgroundColor: theme.primaryLight } : undefined}
className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${ className={`rounded-xl px-4 py-3 flex items-center justify-between transition-all cursor-pointer ${
activeField === 'passenger' activeField === 'passenger'
? 'bg-[#f7faef] border-2 border-dashed border-[#006837] shadow-xs' ? 'border-2 border-dashed shadow-xs'
: 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]' : 'bg-[#f2f3f5] border border-transparent hover:bg-[#eaecef]'
}`} }`}
> >
<div className="flex items-center gap-3 flex-1"> <div className="flex items-center gap-3 flex-1">
<span className="material-icons text-[21px] text-[#006837] flex-shrink-0"> <span className="material-icons text-[21px] flex-shrink-0" style={{ color: theme.primary }}>
person person
</span> </span>
<span className="text-[13.5px] text-gray-800 font-normal"> <span className="text-[13.5px] text-gray-800 font-normal">
@@ -512,7 +614,7 @@ export default function TimetableSearchForm({
{passengers[0]?.discountCard && passengers[0].discountCard !== 'Carte de réduction (optionnel)' ? `, ${passengers[0].discountCard}` : ', sans carte'} {passengers[0]?.discountCard && passengers[0].discountCard !== 'Carte de réduction (optionnel)' ? `, ${passengers[0].discountCard}` : ', sans carte'}
</span> </span>
</div> </div>
<span className="material-icons text-[20px] text-[#006837]"> <span className="material-icons text-[20px]" style={{ color: theme.primary }}>
add add
</span> </span>
</div> </div>
@@ -521,7 +623,8 @@ export default function TimetableSearchForm({
<div className="flex justify-end pt-2"> <div className="flex justify-end pt-2">
<button <button
type="submit" type="submit"
className="w-full sm:w-auto text-white font-semibold px-8 py-2.5 rounded-lg text-[14px] shadow-sm transition-transform active:scale-98 hover:bg-[#00522b] cursor-pointer text-center bg-[#006837]" style={{ backgroundColor: theme.primary }}
className="w-full sm:w-auto text-white font-semibold px-8 py-2.5 rounded-lg text-[14px] shadow-sm transition-transform active:scale-98 hover:opacity-90 cursor-pointer text-center"
> >
Rechercher Rechercher
</button> </button>
@@ -571,10 +674,16 @@ export default function TimetableSearchForm({
<div <div
key={st + idx} key={st + idx}
onClick={() => handleSelectStation(st)} onClick={() => handleSelectStation(st)}
style={isFirst ? {
backgroundColor: theme.primaryLight,
borderColor: theme.primary,
} : {
borderColor: `${theme.primary}25`,
}}
className={`transition-all cursor-pointer ${ className={`transition-all cursor-pointer ${
isFirst isFirst
? 'bg-[#e6f0d7] border border-dashed border-[#006837] rounded-xl p-3 text-[15px] font-medium text-gray-900 shadow-2xs' ? 'border border-dashed 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' : 'py-2.5 px-2 border-b text-[15px] text-gray-900 hover:opacity-80 transition-colors font-medium'
}`} }`}
> >
<span>{st}</span> <span>{st}</span>
@@ -655,11 +764,12 @@ export default function TimetableSearchForm({
type="button" type="button"
disabled={isPastDate} disabled={isPastDate}
onClick={() => !isPastDate && handleSelectDay(dayNum)} onClick={() => !isPastDate && handleSelectDay(dayNum)}
style={isSelected ? { backgroundColor: theme.secondary || theme.primary, color: '#111' } : undefined}
className={`h-9 w-9 mx-auto rounded-full flex items-center justify-center font-normal transition-all ${ className={`h-9 w-9 mx-auto rounded-full flex items-center justify-center font-normal transition-all ${
isPastDate isPastDate
? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50' ? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50'
: isSelected : isSelected
? 'bg-[#c4d600] text-gray-900 font-bold shadow-xs scale-105' ? 'font-bold shadow-xs scale-105'
: 'hover:bg-gray-100 text-gray-800 cursor-pointer' : 'hover:bg-gray-100 text-gray-800 cursor-pointer'
}`} }`}
title={isPastDate ? 'Date passée' : undefined} title={isPastDate ? 'Date passée' : undefined}
@@ -693,12 +803,19 @@ export default function TimetableSearchForm({
<div <div
key={slot} key={slot}
onClick={() => !isPastTime && handleSelectTime(slot)} onClick={() => !isPastTime && handleSelectTime(slot)}
style={isSelected ? {
backgroundColor: theme.primaryLight,
color: theme.primary,
borderColor: theme.primary,
} : {
borderColor: `${theme.primary}25`,
}}
className={`py-2.5 px-4 text-[15px] transition-colors ${ className={`py-2.5 px-4 text-[15px] transition-colors ${
isPastTime isPastTime
? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50 line-through border-b border-gray-100' ? 'text-gray-300 opacity-40 cursor-not-allowed bg-gray-50 line-through border-b border-gray-100'
: isSelected : isSelected
? 'bg-[#e5ecc5] font-semibold text-gray-900 cursor-pointer' ? 'font-bold cursor-pointer border-b'
: 'border-b border-[#006837] text-gray-900 hover:bg-[#f4f8ec] cursor-pointer' : 'border-b text-gray-900 hover:opacity-80 cursor-pointer'
}`} }`}
title={isPastTime ? 'Heure passée' : undefined} title={isPastTime ? 'Heure passée' : undefined}
> >
@@ -722,7 +839,10 @@ export default function TimetableSearchForm({
<div key={p.id} className="space-y-3 relative"> <div key={p.id} className="space-y-3 relative">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-[#006837] text-white flex items-center justify-center"> <span
className="w-6 h-6 rounded-full text-white flex items-center justify-center"
style={{ backgroundColor: theme.primary }}
>
<span className="material-icons text-[15px]">person</span> <span className="material-icons text-[15px]">person</span>
</span> </span>
<span className="text-[14px] font-medium text-gray-900"> <span className="text-[14px] font-medium text-gray-900">
@@ -740,8 +860,11 @@ export default function TimetableSearchForm({
)} )}
</div> </div>
{/* Field 1: Âge (optionnel) - Box with dashed green border matching Image 1 */} {/* Field 1: Âge (optionnel) - Box with dashed theme border */}
<div className="border-2 border-dashed border-[#006837] rounded-lg p-2 bg-[#fff] relative"> <div
className="border-2 border-dashed rounded-lg p-2 bg-[#fff] relative"
style={{ borderColor: theme.primary }}
>
<label className="text-[11px] text-gray-600 font-medium block"> <label className="text-[11px] text-gray-600 font-medium block">
Âge (optionnel) Âge (optionnel)
</label> </label>
@@ -768,7 +891,7 @@ export default function TimetableSearchForm({
onChange={(e) => handleUpdatePassenger(p.id, 'discountCard', e.target.value)} onChange={(e) => handleUpdatePassenger(p.id, 'discountCard', e.target.value)}
className="w-full bg-transparent text-[13px] font-medium text-gray-800 outline-none cursor-pointer appearance-none pr-6" className="w-full bg-transparent text-[13px] font-medium text-gray-800 outline-none cursor-pointer appearance-none pr-6"
style={{ style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23006837' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E")`, backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='${encodeURIComponent(theme.primary)}' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E")`,
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 0 center', backgroundPosition: 'right 0 center',
backgroundSize: '20px' backgroundSize: '20px'
@@ -783,15 +906,19 @@ export default function TimetableSearchForm({
))} ))}
</div> </div>
{/* Yellow Horizontal Divider Line (CONFORME IMG 1) */} {/* Theme Divider Line */}
<div className="w-full border-b border-[#d1d800] my-3"></div> <div
className="w-full border-b my-3"
style={{ borderColor: theme.secondary || '#d1d800' }}
></div>
{/* + Ajouter un voyageur Green Button (CONFORME IMG 1) */} {/* + Ajouter un voyageur Button */}
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
type="button" type="button"
onClick={handleAddPassenger} onClick={handleAddPassenger}
className="flex items-center gap-1.5 text-[#006837] text-[13.5px] font-semibold cursor-pointer hover:underline" className="flex items-center gap-1.5 text-[13.5px] font-semibold cursor-pointer hover:underline"
style={{ color: theme.primary }}
> >
<span className="text-[18px] font-bold">+</span> <span className="text-[18px] font-bold">+</span>
<span>Ajouter un voyageur</span> <span>Ajouter un voyageur</span>
@@ -807,4 +934,3 @@ export default function TimetableSearchForm({
</div> </div>
); );
} }
+79 -16
View File
@@ -3,93 +3,156 @@ export interface RegionTheme {
primaryHover: string; primaryHover: string;
primaryLight: string; primaryLight: string;
secondary: string; secondary: string;
bannerBg: string;
badgeText: string; badgeText: string;
operatorLabel: string;
regionName: string;
heroImage: string;
} }
export const REGION_THEMES: Record<string, RegionTheme> = { export const REGION_THEMES: Record<string, RegionTheme> = {
'bourgogne-franche-comte': { 'bourgogne-franche-comte': {
primary: '#006837', primary: '#006837',
primaryHover: '#00522b', primaryHover: '#00522b',
primaryLight: '#eef5e6', primaryLight: '#edf4d2',
secondary: '#8da300', secondary: '#8ba81b',
bannerBg: '#8ba81b',
badgeText: 'TER Bourgogne-Franche-Comté', badgeText: 'TER Bourgogne-Franche-Comté',
operatorLabel: 'MOBIGO',
regionName: 'Bourgogne-Franche-Comté',
heroImage: 'https://images.unsplash.com/photo-1500382017468-9049fed747ef?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'occitanie': { 'occitanie': {
primary: '#c0001a', primary: '#c0001a',
primaryHover: '#990014', primaryHover: '#990014',
primaryLight: '#fde8ea', primaryLight: '#fde8ea',
secondary: '#e60000', secondary: '#e60000',
bannerBg: '#c0001a',
badgeText: 'liO Train Occitanie', badgeText: 'liO Train Occitanie',
operatorLabel: 'liO',
regionName: 'Occitanie',
heroImage: 'https://images.unsplash.com/photo-1544816155-12df9643f363?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'provence-alpes-cote-azur': { 'provence-alpes-cote-azur': {
primary: '#005088', primary: '#005088',
primaryHover: '#003a63', primaryHover: '#003a63',
primaryLight: '#e5f0f8', primaryLight: '#e5f0f8',
secondary: '#009fe3', secondary: '#ffd100',
bannerBg: '#005088',
badgeText: 'ZOU ! TER PACA', badgeText: 'ZOU ! TER PACA',
operatorLabel: 'ZOU !',
regionName: "Provence-Alpes-Côte d'Azur",
heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'sud-provence-alpes-cote-d-azur': { 'sud-provence-alpes-cote-d-azur': {
primary: '#005088', primary: '#005088',
primaryHover: '#003a63', primaryHover: '#003a63',
primaryLight: '#e5f0f8', primaryLight: '#e5f0f8',
secondary: '#009fe3', secondary: '#ffd100',
bannerBg: '#005088',
badgeText: 'ZOU ! TER PACA', badgeText: 'ZOU ! TER PACA',
operatorLabel: 'ZOU !',
regionName: "Provence-Alpes-Côte d'Azur",
heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'bretagne': { 'bretagne': {
primary: '#0077b6', primary: '#0077b6',
primaryHover: '#005b8c', primaryHover: '#005b8c',
primaryLight: '#e6f4fa', primaryLight: '#e6f4fa',
secondary: '#0096c7', secondary: '#0096c7',
bannerBg: '#0077b6',
badgeText: 'TER BreizhGo', badgeText: 'TER BreizhGo',
operatorLabel: 'BreizhGo',
regionName: 'Bretagne',
heroImage: 'https://images.unsplash.com/photo-1506929562872-bb421503ef21?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'grand-est': { 'grand-est': {
primary: '#2b5c8f', primary: '#0082c8',
primaryHover: '#1e4267', primaryHover: '#00629b',
primaryLight: '#eaf0f6', primaryLight: '#eaf4fb',
secondary: '#4885c1', secondary: '#4885c1',
bannerBg: '#0082c8',
badgeText: 'TER Fluo Grand Est', badgeText: 'TER Fluo Grand Est',
operatorLabel: 'Fluo Grand Est',
regionName: 'Grand Est',
heroImage: 'https://images.unsplash.com/photo-1518684079-3c830dcef090?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'normandie': { 'normandie': {
primary: '#d32f2f', primary: '#d32f2f',
primaryHover: '#b71c1c', primaryHover: '#b71c1c',
primaryLight: '#fbe9e7', primaryLight: '#fbe9e7',
secondary: '#e53935', secondary: '#005c94',
bannerBg: '#005c94',
badgeText: 'TER Nomad Normandie', badgeText: 'TER Nomad Normandie',
operatorLabel: 'NOMAD',
regionName: 'Normandie',
heroImage: 'https://images.unsplash.com/photo-1509316975850-ff9c5deb0cd9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'hauts-de-france': { 'hauts-de-france': {
primary: '#004b87', primary: '#004b87',
primaryHover: '#003663', primaryHover: '#003663',
primaryLight: '#e5edf5', primaryLight: '#e5edf5',
secondary: '#0066b3', secondary: '#0066b3',
bannerBg: '#004b87',
badgeText: 'TER Hauts-de-France', badgeText: 'TER Hauts-de-France',
operatorLabel: 'Hauts-de-France',
regionName: 'Hauts-de-France',
heroImage: 'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'pays-de-la-loire': { 'pays-de-la-loire': {
primary: '#008459', primary: '#008459',
primaryHover: '#006343', primaryHover: '#006343',
primaryLight: '#e6f5ef', primaryLight: '#e6f5ef',
secondary: '#00a872', secondary: '#00a872',
bannerBg: '#008459',
badgeText: 'TER Aléop', badgeText: 'TER Aléop',
operatorLabel: 'Aléop',
regionName: 'Pays de la Loire',
heroImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'nouvelle-aquitaine': { 'nouvelle-aquitaine': {
primary: '#0066a4', primary: '#008542',
primaryHover: '#004c7a', primaryHover: '#006633',
primaryLight: '#e6f2fa', primaryLight: '#e6f5ef',
secondary: '#0085d0', secondary: '#8ba81b',
bannerBg: '#008542',
badgeText: 'TER Nouvelle-Aquitaine', badgeText: 'TER Nouvelle-Aquitaine',
operatorLabel: 'Nouvelle-Aquitaine',
regionName: 'Nouvelle-Aquitaine',
heroImage: 'https://images.unsplash.com/photo-1502082553048-f009c37129b9?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'centre-val-de-loire': { 'centre-val-de-loire': {
primary: '#008080', primary: '#e2001a',
primaryHover: '#006666', primaryHover: '#b50015',
primaryLight: '#e6f2f2', primaryLight: '#fde8ea',
secondary: '#00a3a3', secondary: '#008080',
bannerBg: '#e2001a',
badgeText: 'TER Rémi', badgeText: 'TER Rémi',
operatorLabel: 'Rémi',
regionName: 'Centre-Val de Loire',
heroImage: 'https://images.unsplash.com/photo-1516483638261-f4dbaf036963?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
'auvergne-rhone-alpes': { 'auvergne-rhone-alpes': {
primary: '#00609c', primary: '#00609c',
primaryHover: '#004875', primaryHover: '#004875',
primaryLight: '#e6f1f8', primaryLight: '#e6f1f8',
secondary: '#007bc7', secondary: '#007bc7',
bannerBg: '#00609c',
badgeText: 'TER Auvergne-Rhône-Alpes', badgeText: 'TER Auvergne-Rhône-Alpes',
operatorLabel: 'AURA',
regionName: 'Auvergne-Rhône-Alpes',
heroImage: 'https://images.unsplash.com/photo-1530122037265-a5f1f91d3b99?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
},
'ile-de-france': {
primary: '#0055a5',
primaryHover: '#003e7a',
primaryLight: '#e5f0fa',
secondary: '#009fe3',
bannerBg: '#0055a5',
badgeText: 'Transilien SNCF',
operatorLabel: 'Transilien',
regionName: 'Île-de-France',
heroImage: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80',
}, },
}; };