Compare commits

..

6 Commits

Author SHA1 Message Date
David
5a54940424 chore: sync main with preprod (remove smoke tests + latest changes)
Some checks failed
CD Production / Backend — Lint (push) Successful in 10m22s
CD Production / Frontend — Lint & Type-check (push) Successful in 10m53s
CD Production / Backend — Unit Tests (push) Successful in 10m10s
CD Production / Frontend — Unit Tests (push) Successful in 10m30s
CD Production / Verify Preprod Image Exists (push) Failing after 9s
CD Production / Promote Images (preprod-SHA → prod) (push) Has been skipped
CD Production / Deploy to Production (k3s) (push) Has been skipped
CD Production / Notify Success (push) Has been skipped
CD Production / Notify Failure (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:13:51 +02:00
David
ce8a1049dd fix(cicd): sync corrected pipelines from cicd branch
Some checks failed
CD Production / Frontend — Lint & Type-check (push) Failing after 6m11s
CD Production / Frontend — Unit Tests (push) Has been skipped
CD Production / Backend — Lint (push) Successful in 10m24s
CD Production / Backend — Unit Tests (push) Failing after 5m32s
CD Production / Verify Preprod Image Exists (push) Has been skipped
CD Production / Promote Images (preprod-SHA → prod) (push) Has been skipped
CD Production / Deploy to Production (k3s) (push) Has been skipped
CD Production / Smoke Tests (push) Has been skipped
CD Production / Notify Success (push) Has been skipped
CD Production / Notify Failure (push) Has been skipped
2026-04-04 13:16:48 +02:00
David
9c511c0619 revert: restore root-level docs mistakenly deleted
Some checks failed
CD Production (Hetzner k3s) / Promote Images (preprod → prod) (push) Successful in 31s
CD Production (Hetzner k3s) / Deploy to k3s (xpeditis-prod) (push) Has been cancelled
CD Production (Hetzner k3s) / Smoke Tests (push) Has been cancelled
CD Production (Hetzner k3s) / Deployment Summary (push) Has been cancelled
CD Production (Hetzner k3s) / Notify Success (push) Has been cancelled
CD Production (Hetzner k3s) / Notify Failure (push) Has been cancelled
2026-04-04 13:02:26 +02:00
David
9a79777e34 chore: remove stale root-level docs (already in docs/installation/)
Some checks are pending
CD Production (Hetzner k3s) / Promote Images (preprod → prod) (push) Waiting to run
CD Production (Hetzner k3s) / Deploy to k3s (xpeditis-prod) (push) Blocked by required conditions
CD Production (Hetzner k3s) / Smoke Tests (push) Blocked by required conditions
CD Production (Hetzner k3s) / Deployment Summary (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Success (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Failure (push) Blocked by required conditions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:58:28 +02:00
David
d65cb721b5 chore: sync full codebase from cicd branch
Some checks are pending
CD Production (Hetzner k3s) / Promote Images (preprod → prod) (push) Waiting to run
CD Production (Hetzner k3s) / Deploy to k3s (xpeditis-prod) (push) Blocked by required conditions
CD Production (Hetzner k3s) / Smoke Tests (push) Blocked by required conditions
CD Production (Hetzner k3s) / Deployment Summary (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Success (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Failure (push) Blocked by required conditions
Aligns main with the complete application codebase (cicd branch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:56:44 +02:00
David
b7f85c9bf9 feat(cicd): sync CI/CD pipeline from cicd branch
Some checks failed
CD Production (Hetzner k3s) / Deployment Summary (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Success (push) Blocked by required conditions
CD Production (Hetzner k3s) / Notify Failure (push) Blocked by required conditions
CD Production (Hetzner k3s) / Deploy to k3s (xpeditis-prod) (push) Blocked by required conditions
CD Production (Hetzner k3s) / Smoke Tests (push) Blocked by required conditions
Security Audit / npm audit (push) Failing after 7s
Security Audit / Dependency Review (push) Has been skipped
CD Production (Hetzner k3s) / Promote Images (preprod → prod) (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:52:56 +02:00
14 changed files with 273 additions and 531 deletions

View File

@ -4,7 +4,6 @@ import { useState, useEffect, useCallback } from 'react';
import { getAllBookings, getAllUsers, deleteAdminDocument } from '@/lib/api/admin'; import { getAllBookings, getAllUsers, deleteAdminDocument } from '@/lib/api/admin';
import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react'; import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { PageHeader } from '@/components/ui/PageHeader';
interface Document { interface Document {
id: string; id: string;
@ -338,10 +337,15 @@ export default function AdminDocumentsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader {/* Header */}
title="Gestion des Documents" <div className="flex items-center justify-between">
description="Liste de tous les documents des devis CSV" <div>
/> <h1 className="text-2xl font-bold text-gray-900">Gestion des Documents</h1>
<p className="mt-1 text-sm text-gray-500">
Liste de tous les documents des devis CSV
</p>
</div>
</div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">

View File

@ -12,7 +12,6 @@ import {
Server, Server,
} from 'lucide-react'; } from 'lucide-react';
import { get, download } from '@/lib/api/client'; import { get, download } from '@/lib/api/client';
import { PageHeader } from '@/components/ui/PageHeader';
const LOGS_PREFIX = '/api/v1/logs'; const LOGS_PREFIX = '/api/v1/logs';
@ -190,45 +189,48 @@ export default function AdminLogsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader {/* Header */}
title="Logs système" <div className="flex items-center justify-between">
description="Visualisation et export des logs applicatifs en temps réel" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Logs système</h1>
<div className="flex items-center gap-2"> <p className="mt-1 text-sm text-gray-500">
Visualisation et export des logs applicatifs en temps réel
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchLogs}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Actualiser
</button>
<div className="relative group">
<button <button
onClick={fetchLogs} disabled={exportLoading || loading}
disabled={loading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-[#10183A] rounded-lg hover:bg-[#1a2550] transition-colors disabled:opacity-50"
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
> >
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} /> <Download className="h-4 w-4" />
<span className="hidden sm:inline">Actualiser</span> {exportLoading ? 'Export...' : 'Exporter'}
</button> </button>
<div className="relative group"> <div className="absolute right-0 mt-1 w-36 bg-white rounded-lg shadow-lg border border-gray-200 z-10 hidden group-hover:block">
<button <button
disabled={exportLoading || loading} onClick={() => handleExport('csv')}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-[#10183A] rounded-lg hover:bg-[#1a2550] transition-colors disabled:opacity-50" className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
> >
<Download className="h-4 w-4" /> Télécharger CSV
<span className="hidden sm:inline">{exportLoading ? 'Export...' : 'Exporter'}</span> </button>
<button
onClick={() => handleExport('json')}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
Télécharger JSON
</button> </button>
<div className="absolute right-0 mt-1 w-36 bg-white rounded-lg shadow-lg border border-gray-200 z-10 hidden group-hover:block">
<button
onClick={() => handleExport('csv')}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
Télécharger CSV
</button>
<button
onClick={() => handleExport('json')}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
Télécharger JSON
</button>
</div>
</div> </div>
</div> </div>
} </div>
/> </div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">

View File

@ -3,7 +3,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getAllOrganizations, verifySiret, approveSiret, rejectSiret } from '@/lib/api/admin'; import { getAllOrganizations, verifySiret, approveSiret, rejectSiret } from '@/lib/api/admin';
import { createOrganization, updateOrganization } from '@/lib/api/organizations'; import { createOrganization, updateOrganization } from '@/lib/api/organizations';
import { PageHeader } from '@/components/ui/PageHeader';
interface Organization { interface Organization {
id: string; id: string;
@ -227,18 +226,21 @@ export default function AdminOrganizationsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader {/* Header */}
title="Organization Management" <div className="flex items-center justify-between">
description="Manage all organizations in the system" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Organization Management</h1>
<button <p className="mt-1 text-sm text-gray-500">
onClick={() => setShowCreateModal(true)} Manage all organizations in the system
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors" </p>
> </div>
+ Create Organization <button
</button> onClick={() => setShowCreateModal(true)}
} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
/> >
+ Create Organization
</button>
</div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (

View File

@ -5,7 +5,6 @@ import { getAllUsers, updateAdminUser, deleteAdminUser } from '@/lib/api/admin';
import { createUser } from '@/lib/api/users'; import { createUser } from '@/lib/api/users';
import { getAllOrganizations } from '@/lib/api/admin'; import { getAllOrganizations } from '@/lib/api/admin';
import type { UserRole } from '@/types/api'; import type { UserRole } from '@/types/api';
import { PageHeader } from '@/components/ui/PageHeader';
interface User { interface User {
id: string; id: string;
@ -161,18 +160,21 @@ export default function AdminUsersPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader {/* Header */}
title="User Management" <div className="flex items-center justify-between">
description="Manage all users in the system" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">User Management</h1>
<button <p className="mt-1 text-sm text-gray-500">
onClick={() => setShowCreateModal(true)} Manage all users in the system
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors" </p>
> </div>
+ Create User <button
</button> onClick={() => setShowCreateModal(true)}
} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
/> >
+ Create User
</button>
</div>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (

View File

@ -13,7 +13,6 @@ import Link from 'next/link';
import { Plus, Clock } from 'lucide-react'; import { Plus, Clock } from 'lucide-react';
import ExportButton from '@/components/ExportButton'; import ExportButton from '@/components/ExportButton';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { PageHeader } from '@/components/ui/PageHeader';
type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote'; type SearchType = 'pallets' | 'weight' | 'route' | 'status' | 'date' | 'quote';
@ -167,43 +166,44 @@ export default function BookingsListPage() {
<button onClick={() => setShowTransferBanner(false)} className="text-amber-500 hover:text-amber-700 ml-4 flex-shrink-0"></button> <button onClick={() => setShowTransferBanner(false)} className="text-amber-500 hover:text-amber-700 ml-4 flex-shrink-0"></button>
</div> </div>
)} )}
<PageHeader {/* Header */}
title="Réservations" <div className="flex items-center justify-between">
description="Gérez et suivez vos envois" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Réservations</h1>
<> <p className="text-sm text-gray-500 mt-1">Gérez et suivez vos envois</p>
<ExportButton </div>
data={filteredBookings} <div className="flex items-center space-x-3">
filename="reservations" <ExportButton
columns={[ data={filteredBookings}
{ key: 'id', label: 'ID' }, filename="reservations"
{ key: 'palletCount', label: 'Palettes', format: (v) => `${v || 0}` }, columns={[
{ key: 'weightKG', label: 'Poids (kg)', format: (v) => `${v || 0}` }, { key: 'id', label: 'ID' },
{ key: 'volumeCBM', label: 'Volume (CBM)', format: (v) => `${v || 0}` }, { key: 'palletCount', label: 'Palettes', format: (v) => `${v || 0}` },
{ key: 'origin', label: 'Origine' }, { key: 'weightKG', label: 'Poids (kg)', format: (v) => `${v || 0}` },
{ key: 'destination', label: 'Destination' }, { key: 'volumeCBM', label: 'Volume (CBM)', format: (v) => `${v || 0}` },
{ key: 'carrierName', label: 'Transporteur' }, { key: 'origin', label: 'Origine' },
{ key: 'status', label: 'Statut', format: (v) => { { key: 'destination', label: 'Destination' },
const labels: Record<string, string> = { { key: 'carrierName', label: 'Transporteur' },
PENDING: 'En attente', { key: 'status', label: 'Statut', format: (v) => {
ACCEPTED: 'Accepté', const labels: Record<string, string> = {
REJECTED: 'Refusé', PENDING: 'En attente',
}; ACCEPTED: 'Accepté',
return labels[v] || v; REJECTED: 'Refusé',
}}, };
{ key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' }, return labels[v] || v;
]} }},
/> { key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
<Link ]}
href="/dashboard/search-advanced" />
className="inline-flex items-center px-3 sm:px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700" <Link
> href="/dashboard/search-advanced"
<Plus className="h-4 w-4 sm:mr-2" /> className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
<span className="hidden sm:inline">Nouvelle Réservation</span> >
</Link> <Plus className="mr-2 h-4 w-4" />
</> Nouvelle Réservation
} </Link>
/> </div>
</div>
{/* Filters */} {/* Filters */}
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@ -284,7 +284,7 @@ export default function BookingsListPage() {
</div> </div>
</div> </div>
{/* Bookings List */} {/* Bookings Table */}
<div className="bg-white rounded-lg shadow overflow-hidden"> <div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? ( {isLoading ? (
<div className="px-6 py-12 text-center text-gray-500"> <div className="px-6 py-12 text-center text-gray-500">
@ -293,62 +293,7 @@ export default function BookingsListPage() {
</div> </div>
) : paginatedBookings && paginatedBookings.length > 0 ? ( ) : paginatedBookings && paginatedBookings.length > 0 ? (
<> <>
{/* Mobile cards */} <div className="overflow-x-auto">
<div className="md:hidden divide-y divide-gray-200">
{paginatedBookings.map((booking: any) => (
<div key={`${booking.type}-${booking.id}`} className="p-4 space-y-3">
<div className="flex items-start justify-between">
<div>
<div className="text-sm font-semibold text-gray-900">
{booking.type === 'csv'
? `${booking.origin}${booking.destination}`
: booking.route || 'N/A'}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{booking.type === 'csv' ? booking.carrierName : booking.carrier || ''}
</div>
</div>
<span className={`px-2 py-1 text-xs font-semibold rounded-full flex-shrink-0 ${getStatusColor(booking.status)}`}>
{getStatusLabel(booking.status)}
</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<div className="text-gray-400 uppercase tracking-wide">Palettes</div>
<div className="font-medium text-gray-900 mt-0.5">
{booking.type === 'csv'
? `${booking.palletCount} pal.`
: `${booking.containers?.length || 0} cont.`}
</div>
</div>
<div>
<div className="text-gray-400 uppercase tracking-wide">Poids</div>
<div className="font-medium text-gray-900 mt-0.5">
{booking.type === 'csv'
? `${booking.weightKG} kg`
: booking.totalWeight ? `${booking.totalWeight} kg` : 'N/A'}
</div>
</div>
<div>
<div className="text-gray-400 uppercase tracking-wide">Date</div>
<div className="font-medium text-gray-900 mt-0.5">
{(booking.createdAt || booking.requestedAt)
? new Date(booking.createdAt || booking.requestedAt).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit' })
: 'N/A'}
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{booking.type === 'csv'
? `Réf: #${booking.bookingId || booking.id.slice(0, 8).toUpperCase()}`
: `Booking: ${booking.bookingNumber || '-'}`}
</div>
</div>
))}
</div>
{/* Desktop table */}
<div className="hidden md:block overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>

View File

@ -5,7 +5,6 @@ import { listCsvBookings, CsvBookingResponse } from '@/lib/api/bookings';
import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react'; import { FileText, Image as ImageIcon, FileEdit, FileSpreadsheet, Paperclip } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import ExportButton from '@/components/ExportButton'; import ExportButton from '@/components/ExportButton';
import { PageHeader } from '@/components/ui/PageHeader';
interface Document { interface Document {
id: string; id: string;
@ -406,45 +405,53 @@ export default function UserDocumentsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader {/* Header */}
title="Mes Documents" <div className="flex items-center justify-between">
description="Gérez tous les documents de vos réservations" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Mes Documents</h1>
<> <p className="mt-1 text-sm text-gray-500">
<ExportButton Gérez tous les documents de vos réservations
data={filteredDocuments} </p>
filename="documents" </div>
columns={[ <div className="flex items-center space-x-3">
{ key: 'fileName', label: 'Nom du fichier' }, <ExportButton
{ key: 'fileType', label: 'Type' }, data={filteredDocuments}
{ key: 'quoteNumber', label: 'N° de Devis' }, filename="documents"
{ key: 'route', label: 'Route' }, columns={[
{ key: 'carrierName', label: 'Transporteur' }, { key: 'fileName', label: 'Nom du fichier' },
{ key: 'status', label: 'Statut', format: (v) => { { key: 'fileType', label: 'Type' },
const labels: Record<string, string> = { { key: 'quoteNumber', label: 'N° de Devis' },
PENDING: 'En attente', { key: 'route', label: 'Route' },
ACCEPTED: 'Accepté', { key: 'carrierName', label: 'Transporteur' },
REJECTED: 'Refusé', { key: 'status', label: 'Statut', format: (v) => {
CANCELLED: 'Annulé', const labels: Record<string, string> = {
}; PENDING: 'En attente',
return labels[v] || v; ACCEPTED: 'Accepté',
}}, REJECTED: 'Refusé',
{ key: 'uploadedAt', label: 'Date d\'ajout', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' }, CANCELLED: 'Annulé',
]} };
/> return labels[v] || v;
<button }},
onClick={handleAddDocumentClick} { key: 'uploadedAt', label: 'Date d\'ajout', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
disabled={bookingsAvailableForDocuments.length === 0} ]}
className="inline-flex items-center px-3 sm:px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" />
> <button
<svg className="w-4 h-4 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> onClick={handleAddDocumentClick}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> disabled={bookingsAvailableForDocuments.length === 0}
</svg> className="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
<span className="hidden sm:inline">Ajouter un document</span> >
</button> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</> <path
} strokeLinecap="round"
/> strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Ajouter un document
</button>
</div>
</div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">

View File

@ -24,8 +24,6 @@ import {
LogOut, LogOut,
Lock, Lock,
Key, Key,
Home,
User,
} from 'lucide-react'; } from 'lucide-react';
import { useSubscription } from '@/lib/context/subscription-context'; import { useSubscription } from '@/lib/context/subscription-context';
import StatusBadge from '@/components/ui/StatusBadge'; import StatusBadge from '@/components/ui/StatusBadge';
@ -185,9 +183,9 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
{/* Main content */} {/* Main content */}
<div className="lg:pl-64"> <div className="lg:pl-64">
{/* Top bar */} {/* Top bar */}
<div className="sticky top-0 z-10 flex items-center justify-between h-14 lg:h-16 px-4 lg:px-6 bg-white border-b"> <div className="sticky top-0 z-10 flex items-center justify-between h-16 px-6 bg-white border-b">
<button <button
className="lg:hidden text-gray-500 hover:text-gray-700 p-1" className="lg:hidden text-gray-500 hover:text-gray-700"
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -200,51 +198,24 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</svg> </svg>
</button> </button>
<div className="flex-1 lg:flex-none"> <div className="flex-1 lg:flex-none">
<h1 className="text-base lg:text-xl font-semibold text-gray-900 ml-3 lg:ml-0"> <h1 className="text-xl font-semibold text-gray-900">
{navigation.find(item => isActive(item.href))?.name || 'Tableau de bord'} {navigation.find(item => isActive(item.href))?.name || 'Tableau de bord'}
</h1> </h1>
</div> </div>
<div className="flex items-center space-x-3 lg:space-x-4"> <div className="flex items-center space-x-4">
{/* Notifications */} {/* Notifications */}
<NotificationDropdown /> <NotificationDropdown />
{/* User Initials */} {/* User Initials */}
<Link href="/dashboard/profile" className="w-8 h-8 lg:w-9 lg:h-9 bg-blue-600 rounded-full flex items-center justify-center text-white text-sm font-semibold hover:bg-blue-700 transition-colors"> <Link href="/dashboard/profile" className="w-9 h-9 bg-blue-600 rounded-full flex items-center justify-center text-white text-sm font-semibold hover:bg-blue-700 transition-colors">
{user?.firstName?.[0]}{user?.lastName?.[0]} {user?.firstName?.[0]}{user?.lastName?.[0]}
</Link> </Link>
</div> </div>
</div> </div>
{/* Page content — extra bottom padding on mobile for bottom nav */} {/* Page content */}
<main className="p-4 lg:p-6 pb-24 lg:pb-6">{children}</main> <main className="p-6">{children}</main>
</div> </div>
{/* Mobile bottom navigation bar */}
<nav className="fixed bottom-0 left-0 right-0 z-30 bg-white border-t border-gray-200 lg:hidden">
<div className="grid grid-cols-5 h-16">
{[
{ href: '/dashboard', icon: Home, label: 'Accueil' },
{ href: '/dashboard/bookings', icon: Package, label: 'Réservations' },
{ href: '/dashboard/documents', icon: FileText, label: 'Documents' },
{ href: '/dashboard/track-trace', icon: Search, label: 'Suivi' },
{ href: '/dashboard/profile', icon: User, label: 'Profil' },
].map((item) => {
const active = item.href === '/dashboard' ? pathname === item.href : pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`flex flex-col items-center justify-center space-y-0.5 transition-colors ${
active ? 'text-blue-600' : 'text-gray-500 hover:text-gray-700'
}`}
>
<item.icon className="w-5 h-5" />
<span className="text-[10px] font-medium leading-tight">{item.label}</span>
</Link>
);
})}
</div>
</nav>
</div> </div>
); );
} }

View File

@ -82,17 +82,17 @@ export default function DashboardPage() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-4 sm:py-8 space-y-4 sm:space-y-6"> <div className="max-w-7xl mx-auto px-6 py-8 space-y-6">
{/* Header - Compact */} {/* Header - Compact */}
<div className="flex items-center justify-between pb-3 sm:pb-4 border-b border-gray-200"> <div className="flex items-center justify-between pb-4 border-b border-gray-200">
<div> <div>
<h1 className="text-xl sm:text-3xl font-semibold text-gray-900">Tableau de Bord</h1> <h1 className="text-3xl font-semibold text-gray-900">Tableau de Bord</h1>
<p className="text-gray-600 mt-0.5 sm:mt-1 text-xs sm:text-sm"> <p className="text-gray-600 mt-1 text-sm">
Vue d'ensemble de vos réservations et performances Vue d'ensemble de vos réservations et performances
</p> </p>
</div> </div>
<div className="flex items-center space-x-2 sm:space-x-3"> <div className="flex items-center space-x-3">
<ExportButton <ExportButton
data={topCarriers || []} data={topCarriers || []}
filename="tableau-de-bord-transporteurs" filename="tableau-de-bord-transporteurs"
@ -108,9 +108,9 @@ export default function DashboardPage() {
]} ]}
/> />
<Link href="/dashboard/search-advanced"> <Link href="/dashboard/search-advanced">
<Button className="bg-blue-600 hover:bg-blue-700 text-white gap-2 shadow-lg font-semibold px-3 sm:px-6 py-2 sm:py-5 text-sm sm:text-base"> <Button className="bg-blue-600 hover:bg-blue-700 text-white gap-2 shadow-lg text-base px-6 py-5 font-semibold">
<Plus className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" /> <Plus className="h-5 w-5" />
<span className="hidden sm:inline">Nouvelle Réservation</span> Nouvelle Réservation
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@ -17,7 +17,6 @@ import {
ShieldCheck, ShieldCheck,
Lock, Lock,
} from 'lucide-react'; } from 'lucide-react';
import { PageHeader } from '@/components/ui/PageHeader';
// ─── Helpers ──────────────────────────────────────────────────────────────── // ─── Helpers ────────────────────────────────────────────────────────────────
@ -377,20 +376,23 @@ export default function ApiKeysPage() {
/> />
)} )}
<PageHeader {/* Page header */}
title="Clés API" <div className="flex items-start justify-between mb-8">
description="Gérez les clés d'accès programmatique à l'API Xpeditis." <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Clés API</h1>
<button <p className="mt-1 text-sm text-gray-500">
onClick={() => setShowCreateModal(true)} Gérez les clés d&apos;accès programmatique à l&apos;API Xpeditis.
disabled={activeKeys.length >= 20} </p>
className="flex items-center gap-2 px-4 py-2.5 bg-[#10183A] hover:bg-[#1a2550] disabled:opacity-50 text-white text-sm font-medium rounded-xl transition-colors" </div>
> <button
<Plus className="w-4 h-4" /> onClick={() => setShowCreateModal(true)}
Nouvelle clé disabled={activeKeys.length >= 20}
</button> className="flex items-center gap-2 px-4 py-2.5 bg-[#10183A] hover:bg-[#1a2550] disabled:opacity-50 text-white text-sm font-medium rounded-xl transition-colors"
} >
/> <Plus className="w-4 h-4" />
Nouvelle clé
</button>
</div>
{/* Info banner */} {/* Info banner */}
<div className="mb-6 p-4 bg-blue-50 border border-blue-100 rounded-xl flex gap-3"> <div className="mb-6 p-4 bg-blue-50 border border-blue-100 rounded-xl flex gap-3">

View File

@ -14,7 +14,6 @@ import { createInvitation, listInvitations, cancelInvitation } from '@/lib/api/i
import { useAuth } from '@/lib/context/auth-context'; import { useAuth } from '@/lib/context/auth-context';
import Link from 'next/link'; import Link from 'next/link';
import ExportButton from '@/components/ExportButton'; import ExportButton from '@/components/ExportButton';
import { PageHeader } from '@/components/ui/PageHeader';
const PAGE_SIZE = 5; const PAGE_SIZE = 5;
@ -280,45 +279,44 @@ export default function UsersManagementPage() {
</div> </div>
)} )}
<PageHeader {/* Header */}
title="Gestion des Utilisateurs" <div className="flex items-center justify-between">
description="Gérez les membres de l'équipe et leurs permissions" <div>
actions={ <h1 className="text-2xl font-bold text-gray-900">Gestion des Utilisateurs</h1>
<> <p className="text-sm text-gray-500 mt-1">Gérez les membres de l'équipe et leurs permissions</p>
<ExportButton </div>
data={allUsers} <div className="flex items-center space-x-3">
filename="utilisateurs" <ExportButton
columns={[ data={allUsers}
{ key: 'firstName', label: 'Prénom' }, filename="utilisateurs"
{ key: 'lastName', label: 'Nom' }, columns={[
{ key: 'email', label: 'Email' }, { key: 'firstName', label: 'Prénom' },
{ key: 'role', label: 'Rôle', format: (v) => ({ ADMIN: 'Administrateur', MANAGER: 'Manager', USER: 'Utilisateur', VIEWER: 'Lecteur' }[v] || v) }, { key: 'lastName', label: 'Nom' },
{ key: 'isActive', label: 'Statut', format: (v) => v ? 'Actif' : 'Inactif' }, { key: 'email', label: 'Email' },
{ key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' }, { key: 'role', label: 'Rôle', format: (v) => ({ ADMIN: 'Administrateur', MANAGER: 'Manager', USER: 'Utilisateur', VIEWER: 'Lecteur' }[v] || v) },
]} { key: 'isActive', label: 'Statut', format: (v) => v ? 'Actif' : 'Inactif' },
/> { key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
{licenseStatus?.canInvite ? ( ]}
<button />
onClick={() => setShowInviteModal(true)} {licenseStatus?.canInvite ? (
className="inline-flex items-center px-3 sm:px-4 py-2 text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700" <button
> onClick={() => setShowInviteModal(true)}
<span className="mr-1.5">+</span> className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
<span className="hidden sm:inline">Inviter un utilisateur</span> >
<span className="sm:hidden">Inviter</span> <span className="mr-2">+</span>
</button> Inviter un utilisateur
) : ( </button>
<Link ) : (
href="/dashboard/settings/subscription" <Link
className="inline-flex items-center px-3 sm:px-4 py-2 text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700" href="/dashboard/settings/subscription"
> className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700"
<span className="mr-1.5">+</span> >
<span className="hidden sm:inline">Mettre à niveau</span> <span className="mr-2">+</span>
<span className="sm:hidden">Upgrade</span> Mettre à niveau
</Link> </Link>
)} )}
</> </div>
} </div>
/>
{success && ( {success && (
<div className="rounded-md bg-green-50 p-4"> <div className="rounded-md bg-green-50 p-4">

View File

@ -346,7 +346,7 @@ export default function LandingPage() {
<div className="absolute inset-0 bg-gradient-to-br from-brand-navy/70 via-brand-navy/50 to-brand-turquoise/20" /> <div className="absolute inset-0 bg-gradient-to-br from-brand-navy/70 via-brand-navy/50 to-brand-turquoise/20" />
</motion.div> </motion.div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20"> <div className="relative z-10 max-w-7xl mx-auto px-6 lg:px-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isHeroInView ? { opacity: 1, y: 0 } : {}} animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
@ -357,10 +357,10 @@ export default function LandingPage() {
initial={{ scale: 0.8, opacity: 0 }} initial={{ scale: 0.8, opacity: 0 }}
animate={isHeroInView ? { scale: 1, opacity: 1 } : {}} animate={isHeroInView ? { scale: 1, opacity: 1 } : {}}
transition={{ duration: 0.6, delay: 0.2 }} transition={{ duration: 0.6, delay: 0.2 }}
className="inline-flex items-center space-x-2 bg-white/10 backdrop-blur-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full mb-6 sm:mb-8 border border-white/20" className="inline-flex items-center space-x-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full mb-8 border border-white/20"
> >
<Ship className="w-4 h-4 sm:w-5 sm:h-5 text-brand-turquoise flex-shrink-0" /> <Ship className="w-5 h-5 text-brand-turquoise" />
<span className="text-white/90 text-xs sm:text-sm font-medium"> <span className="text-white/90 text-sm font-medium">
Plateforme B2B de Fret Maritime #1 en Europe Plateforme B2B de Fret Maritime #1 en Europe
</span> </span>
</motion.div> </motion.div>
@ -369,7 +369,7 @@ export default function LandingPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isHeroInView ? { opacity: 1, y: 0 } : {}} animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.3 }} transition={{ duration: 0.8, delay: 0.3 }}
className="text-3xl sm:text-5xl lg:text-7xl font-bold text-white mb-4 sm:mb-6 leading-tight" className="text-5xl lg:text-7xl font-bold text-white mb-6 leading-tight"
> >
Réservez votre fret Réservez votre fret
<br /> <br />
@ -382,7 +382,7 @@ export default function LandingPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isHeroInView ? { opacity: 1, y: 0 } : {}} animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.4 }} transition={{ duration: 0.8, delay: 0.4 }}
className="text-base sm:text-xl lg:text-2xl text-white/80 mb-8 sm:mb-12 max-w-3xl mx-auto leading-relaxed px-2" className="text-xl lg:text-2xl text-white/80 mb-12 max-w-3xl mx-auto leading-relaxed"
> >
Comparez les tarifs de 50+ compagnies maritimes, réservez en ligne et suivez vos Comparez les tarifs de 50+ compagnies maritimes, réservez en ligne et suivez vos
envois en temps réel. envois en temps réel.
@ -392,14 +392,14 @@ export default function LandingPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={isHeroInView ? { opacity: 1, y: 0 } : {}} animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.5 }} transition={{ duration: 0.8, delay: 0.5 }}
className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 mb-12 px-4 sm:px-0" className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-6 mb-12"
> >
{isAuthenticated && user ? ( {isAuthenticated && user ? (
<Link <Link
href="/dashboard" href="/dashboard"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Accéder au tableau de bord</span> <span>Accéder au tableau de bord</span>
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
@ -410,7 +410,7 @@ export default function LandingPage() {
href="/register" href="/register"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Créer un compte gratuit</span> <span>Créer un compte gratuit</span>
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
@ -419,7 +419,7 @@ export default function LandingPage() {
href="/login" href="/login"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-6 sm:px-8 py-3 sm:py-4 bg-white text-brand-navy rounded-lg hover:bg-gray-50 transition-all hover:shadow-xl font-semibold text-base sm:text-lg w-full sm:w-auto text-center" className="px-8 py-4 bg-white text-brand-navy rounded-lg hover:bg-gray-50 transition-all hover:shadow-xl font-semibold text-lg w-full sm:w-auto"
> >
Voir la démo Voir la démo
</Link> </Link>
@ -452,12 +452,12 @@ export default function LandingPage() {
</section> </section>
{/* Stats Section */} {/* Stats Section */}
<section ref={statsRef} className="py-10 sm:py-16 bg-gray-50"> <section ref={statsRef} className="py-16 bg-gray-50">
<motion.div <motion.div
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={isStatsInView ? 'visible' : 'hidden'} animate={isStatsInView ? 'visible' : 'hidden'}
className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" className="max-w-7xl mx-auto px-6 lg:px-8"
> >
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{stats.map((stat, index) => { {stats.map((stat, index) => {
@ -477,7 +477,7 @@ export default function LandingPage() {
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={isStatsInView ? { scale: 1 } : {}} animate={isStatsInView ? { scale: 1 } : {}}
transition={{ duration: 0.5, delay: index * 0.1 }} transition={{ duration: 0.5, delay: index * 0.1 }}
className="text-3xl sm:text-5xl lg:text-6xl font-bold text-brand-navy mb-2 tabular-nums" className="text-5xl lg:text-6xl font-bold text-brand-navy mb-2 tabular-nums"
> >
<AnimatedCounter <AnimatedCounter
end={stat.end} end={stat.end}
@ -497,18 +497,18 @@ export default function LandingPage() {
</section> </section>
{/* Features Section */} {/* Features Section */}
<section ref={featuresRef} id="features" className="py-12 sm:py-20 lg:py-32"> <section ref={featuresRef} id="features" className="py-20 lg:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isFeaturesInView ? { opacity: 1, y: 0 } : {}} animate={isFeaturesInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="text-center mb-10 sm:mb-16" className="text-center mb-16"
> >
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4"> <h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4">
Pourquoi choisir Xpeditis ? Pourquoi choisir Xpeditis ?
</h2> </h2>
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-xl text-gray-600 max-w-2xl mx-auto">
Une plateforme complète pour gérer tous vos besoins en fret maritime Une plateforme complète pour gérer tous vos besoins en fret maritime
</p> </p>
</motion.div> </motion.div>
@ -620,23 +620,23 @@ export default function LandingPage() {
<section <section
ref={pricingRef} ref={pricingRef}
id="pricing" id="pricing"
className="py-12 sm:py-20 lg:py-32 bg-gradient-to-b from-white to-gray-50" className="py-20 lg:py-32 bg-gradient-to-b from-white to-gray-50"
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">
{/* Header */} {/* Header */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isPricingInView ? { opacity: 1, y: 0 } : {}} animate={isPricingInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="text-center mb-8 sm:mb-12" className="text-center mb-12"
> >
<span className="inline-block bg-brand-turquoise/10 text-brand-turquoise text-sm font-semibold px-4 py-1.5 rounded-full mb-4 uppercase tracking-wide"> <span className="inline-block bg-brand-turquoise/10 text-brand-turquoise text-sm font-semibold px-4 py-1.5 rounded-full mb-4 uppercase tracking-wide">
Tarifs Tarifs
</span> </span>
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4"> <h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4">
Des plans adaptés à votre activité Des plans adaptés à votre activité
</h2> </h2>
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-xl text-gray-600 max-w-2xl mx-auto">
De l'accès découverte au partenariat sur mesure évoluez à tout moment. De l'accès découverte au partenariat sur mesure évoluez à tout moment.
</p> </p>
</motion.div> </motion.div>
@ -959,19 +959,19 @@ export default function LandingPage() {
{/* Testimonials Section */} {/* Testimonials Section */}
<section <section
ref={testimonialsRef} ref={testimonialsRef}
className="py-12 sm:py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white" className="py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white"
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isTestimonialsInView ? { opacity: 1, y: 0 } : {}} animate={isTestimonialsInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="text-center mb-10 sm:mb-16" className="text-center mb-16"
> >
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4"> <h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4">
Ils nous font confiance Ils nous font confiance
</h2> </h2>
<p className="text-base sm:text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-xl text-gray-600 max-w-2xl mx-auto">
Découvrez les témoignages de nos clients satisfaits Découvrez les témoignages de nos clients satisfaits
</p> </p>
</motion.div> </motion.div>
@ -1014,18 +1014,18 @@ export default function LandingPage() {
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section ref={ctaRef} className="py-12 sm:py-20 lg:py-32"> <section ref={ctaRef} className="py-20 lg:py-32">
<motion.div <motion.div
variants={containerVariants} variants={containerVariants}
initial="hidden" initial="hidden"
animate={isCtaInView ? 'visible' : 'hidden'} animate={isCtaInView ? 'visible' : 'hidden'}
className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center" className="max-w-4xl mx-auto px-6 lg:px-8 text-center"
> >
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-4 sm:mb-6"> <h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-6">
Prêt à simplifier votre fret maritime ? Prêt à simplifier votre fret maritime ?
</h2> </h2>
<p className="text-base sm:text-xl text-gray-600 mb-8 sm:mb-10"> <p className="text-xl text-gray-600 mb-10">
Rejoignez des centaines de transitaires qui font confiance à Xpeditis pour leurs Rejoignez des centaines de transitaires qui font confiance à Xpeditis pour leurs
expéditions maritimes. expéditions maritimes.
</p> </p>
@ -1033,14 +1033,14 @@ export default function LandingPage() {
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 px-2 sm:px-0" className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-6"
> >
{isAuthenticated && user ? ( {isAuthenticated && user ? (
<Link <Link
href="/dashboard" href="/dashboard"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Accéder au tableau de bord</span> <span>Accéder au tableau de bord</span>
<LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <LayoutDashboard className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
@ -1051,7 +1051,7 @@ export default function LandingPage() {
href="/register" href="/register"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group px-6 sm:px-8 py-3 sm:py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-base sm:text-lg w-full sm:w-auto flex items-center justify-center space-x-2" className="group px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all hover:shadow-2xl hover:scale-105 font-semibold text-lg w-full sm:w-auto flex items-center justify-center space-x-2"
> >
<span>Créer un compte gratuit</span> <span>Créer un compte gratuit</span>
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
@ -1060,7 +1060,7 @@ export default function LandingPage() {
href="/login" href="/login"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-6 sm:px-8 py-3 sm:py-4 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 transition-all hover:shadow-xl font-semibold text-base sm:text-lg w-full sm:w-auto text-center" className="px-8 py-4 bg-brand-navy text-white rounded-lg hover:bg-brand-navy/90 transition-all hover:shadow-xl font-semibold text-lg w-full sm:w-auto"
> >
Se connecter Se connecter
</Link> </Link>
@ -1070,7 +1070,7 @@ export default function LandingPage() {
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 mt-8 text-sm text-gray-500" className="flex items-center justify-center space-x-6 mt-8 text-sm text-gray-500"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<CheckCircle2 className="w-4 h-4 text-brand-green" /> <CheckCircle2 className="w-4 h-4 text-brand-green" />

View File

@ -11,7 +11,6 @@ import { motion, AnimatePresence } from 'framer-motion';
import { Cookie, X, Settings, Check, Shield } from 'lucide-react'; import { Cookie, X, Settings, Check, Shield } from 'lucide-react';
import { useCookieConsent } from '@/lib/context/cookie-context'; import { useCookieConsent } from '@/lib/context/cookie-context';
import type { CookiePreferences } from '@/lib/api/gdpr'; import type { CookiePreferences } from '@/lib/api/gdpr';
import { usePathname } from 'next/navigation';
export default function CookieConsent() { export default function CookieConsent() {
const { const {
@ -28,12 +27,6 @@ export default function CookieConsent() {
} = useCookieConsent(); } = useCookieConsent();
const [localPrefs, setLocalPrefs] = useState<CookiePreferences>(preferences); const [localPrefs, setLocalPrefs] = useState<CookiePreferences>(preferences);
const pathname = usePathname();
// On dashboard pages, mobile has a bottom nav bar (h-16 = 64px). Offset to avoid overlap.
const isDashboard = pathname?.startsWith('/dashboard');
// Classes to apply only on mobile when on the dashboard
const mobileOffset = isDashboard ? 'bottom-16 lg:bottom-0' : 'bottom-0';
const mobileButtonOffset = isDashboard ? 'bottom-20 lg:bottom-4' : 'bottom-4';
// Sync local prefs when context changes // Sync local prefs when context changes
React.useEffect(() => { React.useEffect(() => {
@ -60,7 +53,7 @@ export default function CookieConsent() {
exit={{ scale: 0, opacity: 0 }} exit={{ scale: 0, opacity: 0 }}
transition={{ type: 'spring', stiffness: 260, damping: 20 }} transition={{ type: 'spring', stiffness: 260, damping: 20 }}
onClick={openPreferences} onClick={openPreferences}
className={`fixed left-4 z-40 p-3 bg-brand-navy text-white rounded-full shadow-lg hover:bg-brand-navy/90 focus:outline-none focus:ring-2 focus:ring-brand-turquoise focus:ring-offset-2 transition-colors ${mobileButtonOffset}`} className="fixed bottom-4 left-4 z-40 p-3 bg-brand-navy text-white rounded-full shadow-lg hover:bg-brand-navy/90 focus:outline-none focus:ring-2 focus:ring-brand-turquoise focus:ring-offset-2 transition-colors"
aria-label="Ouvrir les paramètres de cookies" aria-label="Ouvrir les paramètres de cookies"
> >
<Cookie className="w-5 h-5" /> <Cookie className="w-5 h-5" />
@ -76,7 +69,7 @@ export default function CookieConsent() {
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }} exit={{ y: 100, opacity: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }} transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className={`fixed left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-2xl ${mobileOffset}`} className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-2xl"
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">

View File

@ -10,11 +10,8 @@ import {
BookOpen, BookOpen,
LayoutDashboard, LayoutDashboard,
Code2, Code2,
Menu,
X,
} from 'lucide-react'; } from 'lucide-react';
import { useAuth } from '@/lib/context/auth-context'; import { useAuth } from '@/lib/context/auth-context';
import { usePathname } from 'next/navigation';
interface LandingHeaderProps { interface LandingHeaderProps {
transparentOnTop?: boolean; transparentOnTop?: boolean;
@ -24,14 +21,7 @@ interface LandingHeaderProps {
export function LandingHeader({ transparentOnTop = false, activePage }: LandingHeaderProps) { export function LandingHeader({ transparentOnTop = false, activePage }: LandingHeaderProps) {
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const [isCompanyMenuOpen, setIsCompanyMenuOpen] = useState(false); const [isCompanyMenuOpen, setIsCompanyMenuOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const { user, isAuthenticated, loading } = useAuth(); const { user, isAuthenticated, loading } = useAuth();
const pathname = usePathname();
// Close mobile menu on route change
useEffect(() => {
setIsMobileMenuOpen(false);
}, [pathname]);
const companyMenuItems = [ const companyMenuItems = [
{ href: '/about', label: 'À propos', icon: Info, description: 'Notre histoire et mission' }, { href: '/about', label: 'À propos', icon: Info, description: 'Notre histoire et mission' },
@ -76,7 +66,6 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH
}; };
return ( return (
<>
<motion.nav <motion.nav
initial={{ y: -100 }} initial={{ y: -100 }}
animate={{ y: 0 }} animate={{ y: 0 }}
@ -84,28 +73,18 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH
isScrolled ? 'bg-brand-navy/95 backdrop-blur-md shadow-lg' : 'bg-transparent' isScrolled ? 'bg-brand-navy/95 backdrop-blur-md shadow-lg' : 'bg-transparent'
}`} }`}
> >
<div className="max-w-7xl mx-auto px-4 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="flex items-center justify-between h-16 md:h-20"> <div className="flex items-center justify-between h-20">
<Link href="/" className="flex items-center space-x-2"> <Link href="/" className="flex items-center space-x-2">
<Image <Image
src="/assets/logos/logo-white.png" src="/assets/logos/logo-white.png"
alt="Xpeditis" alt="Xpeditis"
width={60} width={70}
height={70} height={80}
priority priority
className="h-auto" className="h-auto"
/> />
</Link> </Link>
{/* Mobile hamburger button */}
<button
className="md:hidden p-2 text-white hover:text-brand-turquoise transition-colors"
onClick={() => setIsMobileMenuOpen(true)}
aria-label="Ouvrir le menu"
>
<Menu className="w-6 h-6" />
</button>
<div className="hidden md:flex items-center space-x-8"> <div className="hidden md:flex items-center space-x-8">
<Link <Link
href="/#features" href="/#features"
@ -247,138 +226,5 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH
</div> </div>
</div> </div>
</motion.nav> </motion.nav>
{/* Mobile menu drawer */}
<AnimatePresence>
{isMobileMenuOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-black/60 md:hidden"
onClick={() => setIsMobileMenuOpen(false)}
/>
{/* Drawer */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'tween', duration: 0.25 }}
className="fixed top-0 right-0 bottom-0 z-50 w-[280px] bg-brand-navy flex flex-col md:hidden shadow-2xl"
>
{/* Drawer header */}
<div className="flex items-center justify-between px-5 h-16 border-b border-white/10">
<Image
src="/assets/logos/logo-white.png"
alt="Xpeditis"
width={50}
height={60}
className="h-auto"
/>
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-2 text-white/70 hover:text-white transition-colors"
aria-label="Fermer le menu"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Nav links */}
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
<Link
href="/#features"
className="flex items-center px-4 py-3 text-white/80 hover:text-white hover:bg-white/10 rounded-xl transition-colors font-medium"
>
Fonctionnalités
</Link>
<Link
href="/#pricing"
className="flex items-center px-4 py-3 text-white/80 hover:text-white hover:bg-white/10 rounded-xl transition-colors font-medium"
>
Tarifs
</Link>
{/* Company submenu items (flat on mobile) */}
<div className="pt-2 pb-1 px-4 text-xs text-white/40 uppercase tracking-widest font-medium">
Entreprise
</div>
{companyMenuItems.map((item) => {
const IconComponent = item.icon;
return (
<Link
key={item.href}
href={item.href}
className="flex items-center space-x-3 px-4 py-3 text-white/80 hover:text-white hover:bg-white/10 rounded-xl transition-colors"
>
<IconComponent className="w-4 h-4 text-brand-turquoise" />
<span className="font-medium">{item.label}</span>
</Link>
);
})}
<Link
href="/contact"
className={`flex items-center px-4 py-3 rounded-xl transition-colors font-medium ${
activePage === 'contact'
? 'text-brand-turquoise bg-white/10'
: 'text-white/80 hover:text-white hover:bg-white/10'
}`}
>
Contact
</Link>
<Link
href="/docs/api"
className={`flex items-center space-x-2 px-4 py-3 rounded-xl transition-colors font-medium ${
activePage === 'docs'
? 'text-brand-turquoise bg-white/10'
: 'text-white/80 hover:text-white hover:bg-white/10'
}`}
>
<Code2 className="w-4 h-4" />
<span>Docs API</span>
</Link>
</nav>
{/* Auth section */}
<div className="border-t border-white/10 p-4 space-y-3">
{loading ? (
<div className="h-10 rounded-xl bg-white/10 animate-pulse" />
) : isAuthenticated && user ? (
<Link
href="/dashboard"
className="flex items-center space-x-3 px-4 py-3 bg-brand-turquoise rounded-xl text-white font-medium"
>
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center text-sm font-semibold flex-shrink-0">
{getUserInitials()}
</div>
<span className="flex-1 truncate">{getFullName()}</span>
<LayoutDashboard className="w-4 h-4 flex-shrink-0" />
</Link>
) : (
<>
<Link
href="/login"
className="flex items-center justify-center w-full px-4 py-3 border border-white/30 text-white rounded-xl hover:bg-white/10 transition-colors font-medium"
>
Connexion
</Link>
<Link
href="/register"
className="flex items-center justify-center w-full px-4 py-3 bg-brand-turquoise text-white rounded-xl hover:bg-brand-turquoise/90 transition-colors font-medium"
>
Commencer Gratuitement
</Link>
</>
)}
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
); );
} }

View File

@ -1,30 +0,0 @@
import { ReactNode } from 'react';
interface PageHeaderProps {
title: string;
description?: string;
actions?: ReactNode;
}
/**
* Consistent page header for dashboard pages.
* Mobile: actions appear above title (right-aligned).
* Desktop: title on the left, actions on the right.
*/
export function PageHeader({ title, description, actions }: PageHeaderProps) {
return (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
{actions && (
<div className="flex items-center justify-end gap-2 sm:order-last">
{actions}
</div>
)}
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">{title}</h1>
{description && (
<p className="mt-0.5 text-sm text-gray-500">{description}</p>
)}
</div>
</div>
);
}