V0.5.0.
This commit is contained in:
1 parent
24cb502770
commit
aaec7e8d0f
30 files changed
+5343
-1974
No files matched your search
@@ -5,3 +5,8 @@ const nextConfig: NextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
||||||
|
// next.config.js
|
||||||
|
module.exports = {
|
||||||
|
allowedDevOrigins: ['192.168.1.104'],
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'info trafic...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<InfoTraficHubClient regionSlug={region} />
|
||||||
|
</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 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'info trafic...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<InfoTraficHubClient regionSlug="bourgogne-franche-comte" />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'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'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'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'assistance en gare et les équipements à bord des trains
|
||||||
|
</span>
|
||||||
|
<p className="leading-relaxed">
|
||||||
|
Découvrez comment bénéficier d'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'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 >)</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'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 > Besançon</Link></li>
|
||||||
|
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Laroche-Migennes')} className="hover:underline hover:text-black">Dijon > Laroche-Migennes</Link></li>
|
||||||
|
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Chalon-sur-Saone')} className="hover:underline hover:text-black">Dijon > Chalon-sur-Saône</Link></li>
|
||||||
|
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Paris')} className="hover:underline hover:text-black">Dijon > Paris</Link></li>
|
||||||
|
<li><Link href={getRegionPath('/horaires?depart=Dijon&arrivee=Dole')} className="hover:underline hover:text-black">Dijon > 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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">></span>
|
<span className="text-gray-400">></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>
|
||||||
|
|||||||
@@ -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'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'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'un mouvement social, votre billet est utilisable sur n'importe quel autre train de la même journée, ou remboursable sans frais sur simple demande sur le canal d'achat initial.
|
||||||
|
</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'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'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'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'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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'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'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'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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'art sur le réseau ferré régional.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Ces travaux indispensables permettent de <strong>renforcer la sécurité</strong>, d'<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'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'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'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'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'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'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'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'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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
+1285
-1369
File diff suppressed because it is too large.
Load diff
@@ -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'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 */}
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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'horaires</span>
|
||||||
|
<span className="material-icons text-[16px]">chevron_right</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user