Compare commits

...

4 Commits

Author SHA1 Message Date
David
b352d1d9a9 Merge branch 'dev' into preprod
All checks were successful
CD Preprod / Backend — Lint (push) Successful in 10m24s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 10m54s
CD Preprod / Backend — Unit Tests (push) Successful in 10m12s
CD Preprod / Frontend — Unit Tests (push) Successful in 10m33s
CD Preprod / Backend — Integration Tests (push) Successful in 10m0s
CD Preprod / Build Backend (push) Successful in 57s
CD Preprod / Build Log Exporter (push) Successful in 1m7s
CD Preprod / Build Frontend (push) Successful in 19m38s
CD Preprod / Deploy to Preprod (push) Successful in 25s
CD Preprod / Notify Failure (push) Has been skipped
CD Preprod / Notify Success (push) Successful in 2s
2026-04-13 11:53:43 +02:00
David
8649b8a13c Merge branch 'mobile_app' into dev
All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m26s
Dev CI / Frontend — Lint & Type-check (push) Successful in 10m57s
Dev CI / Backend — Unit Tests (push) Successful in 10m12s
Dev CI / Frontend — Unit Tests (push) Successful in 10m37s
Dev CI / Notify Failure (push) Has been skipped
2026-04-09 17:55:05 +02:00
David
982c893952 fix mobile version 2026-04-09 17:54:48 +02:00
David
be1de882c3 chore: sync dev with preprod
All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Frontend — Lint & Type-check (push) Successful in 10m55s
Dev CI / Backend — Unit Tests (push) Successful in 10m10s
Dev CI / Frontend — Unit Tests (push) Successful in 10m30s
Dev CI / Notify Failure (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:16:16 +02:00
14 changed files with 531 additions and 273 deletions

View File

@ -4,6 +4,7 @@ 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;
@ -337,15 +338,10 @@ export default function AdminDocumentsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Gestion des Documents"
<div> description="Liste de tous les documents des devis CSV"
<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,6 +12,7 @@ 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';
@ -189,30 +190,26 @@ export default function AdminLogsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Logs système"
<div> description="Visualisation et export des logs applicatifs en temps réel"
<h1 className="text-2xl font-bold text-gray-900">Logs système</h1> actions={
<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"> <div className="flex items-center gap-2">
<button <button
onClick={fetchLogs} onClick={fetchLogs}
disabled={loading} 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" 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' : ''}`} /> <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Actualiser <span className="hidden sm:inline">Actualiser</span>
</button> </button>
<div className="relative group"> <div className="relative group">
<button <button
disabled={exportLoading || loading} disabled={exportLoading || 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-white bg-[#10183A] rounded-lg hover:bg-[#1a2550] transition-colors disabled:opacity-50"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
{exportLoading ? 'Export...' : 'Exporter'} <span className="hidden sm:inline">{exportLoading ? 'Export...' : 'Exporter'}</span>
</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"> <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
@ -230,7 +227,8 @@ export default function AdminLogsPage() {
</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,6 +3,7 @@
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;
@ -226,21 +227,18 @@ export default function AdminOrganizationsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Organization Management"
<div> description="Manage all organizations in the system"
<h1 className="text-2xl font-bold text-gray-900">Organization Management</h1> actions={
<p className="mt-1 text-sm text-gray-500">
Manage all organizations in the system
</p>
</div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
> >
+ Create Organization + Create Organization
</button> </button>
</div> }
/>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (

View File

@ -5,6 +5,7 @@ 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;
@ -160,21 +161,18 @@ export default function AdminUsersPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="User Management"
<div> description="Manage all users in the system"
<h1 className="text-2xl font-bold text-gray-900">User Management</h1> actions={
<p className="mt-1 text-sm text-gray-500">
Manage all users in the system
</p>
</div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
> >
+ Create User + Create User
</button> </button>
</div> }
/>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (

View File

@ -13,6 +13,7 @@ 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';
@ -166,13 +167,11 @@ 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>
)} )}
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Réservations"
<div> description="Gérez et suivez vos envois"
<h1 className="text-2xl font-bold text-gray-900">Réservations</h1> actions={
<p className="text-sm text-gray-500 mt-1">Gérez et suivez vos envois</p> <>
</div>
<div className="flex items-center space-x-3">
<ExportButton <ExportButton
data={filteredBookings} data={filteredBookings}
filename="reservations" filename="reservations"
@ -197,13 +196,14 @@ export default function BookingsListPage() {
/> />
<Link <Link
href="/dashboard/search-advanced" href="/dashboard/search-advanced"
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" 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"
> >
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 sm:mr-2" />
Nouvelle Réservation <span className="hidden sm:inline">Nouvelle Réservation</span>
</Link> </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 Table */} {/* Bookings List */}
<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,7 +293,62 @@ export default function BookingsListPage() {
</div> </div>
) : paginatedBookings && paginatedBookings.length > 0 ? ( ) : paginatedBookings && paginatedBookings.length > 0 ? (
<> <>
<div className="overflow-x-auto"> {/* Mobile cards */}
<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,6 +5,7 @@ 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;
@ -405,15 +406,11 @@ export default function UserDocumentsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Mes Documents"
<div> description="Gérez tous les documents de vos réservations"
<h1 className="text-2xl font-bold text-gray-900">Mes Documents</h1> actions={
<p className="mt-1 text-sm text-gray-500"> <>
Gérez tous les documents de vos réservations
</p>
</div>
<div className="flex items-center space-x-3">
<ExportButton <ExportButton
data={filteredDocuments} data={filteredDocuments}
filename="documents" filename="documents"
@ -438,20 +435,16 @@ export default function UserDocumentsPage() {
<button <button
onClick={handleAddDocumentClick} onClick={handleAddDocumentClick}
disabled={bookingsAvailableForDocuments.length === 0} disabled={bookingsAvailableForDocuments.length === 0}
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" 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"
> >
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg> </svg>
Ajouter un document <span className="hidden sm:inline">Ajouter un document</span>
</button> </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,6 +24,8 @@ 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';
@ -183,9 +185,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-16 px-6 bg-white border-b"> <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">
<button <button
className="lg:hidden text-gray-500 hover:text-gray-700" className="lg:hidden text-gray-500 hover:text-gray-700 p-1"
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">
@ -198,24 +200,51 @@ 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-xl font-semibold text-gray-900"> <h1 className="text-base lg:text-xl font-semibold text-gray-900 ml-3 lg:ml-0">
{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-4"> <div className="flex items-center space-x-3 lg:space-x-4">
{/* Notifications */} {/* Notifications */}
<NotificationDropdown /> <NotificationDropdown />
{/* User Initials */} {/* User Initials */}
<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"> <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">
{user?.firstName?.[0]}{user?.lastName?.[0]} {user?.firstName?.[0]}{user?.lastName?.[0]}
</Link> </Link>
</div> </div>
</div> </div>
{/* Page content */} {/* Page content — extra bottom padding on mobile for bottom nav */}
<main className="p-6">{children}</main> <main className="p-4 lg:p-6 pb-24 lg:pb-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-6 py-8 space-y-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 py-4 sm:py-8 space-y-4 sm:space-y-6">
{/* Header - Compact */} {/* Header - Compact */}
<div className="flex items-center justify-between pb-4 border-b border-gray-200"> <div className="flex items-center justify-between pb-3 sm:pb-4 border-b border-gray-200">
<div> <div>
<h1 className="text-3xl font-semibold text-gray-900">Tableau de Bord</h1> <h1 className="text-xl sm:text-3xl font-semibold text-gray-900">Tableau de Bord</h1>
<p className="text-gray-600 mt-1 text-sm"> <p className="text-gray-600 mt-0.5 sm:mt-1 text-xs sm: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-3"> <div className="flex items-center space-x-2 sm: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 text-base px-6 py-5 font-semibold"> <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">
<Plus className="h-5 w-5" /> <Plus className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
Nouvelle Réservation <span className="hidden sm:inline">Nouvelle Réservation</span>
</Button> </Button>
</Link> </Link>
</div> </div>

View File

@ -17,6 +17,7 @@ import {
ShieldCheck, ShieldCheck,
Lock, Lock,
} from 'lucide-react'; } from 'lucide-react';
import { PageHeader } from '@/components/ui/PageHeader';
// ─── Helpers ──────────────────────────────────────────────────────────────── // ─── Helpers ────────────────────────────────────────────────────────────────
@ -376,14 +377,10 @@ export default function ApiKeysPage() {
/> />
)} )}
{/* Page header */} <PageHeader
<div className="flex items-start justify-between mb-8"> title="Clés API"
<div> description="Gérez les clés d'accès programmatique à l'API Xpeditis."
<h1 className="text-2xl font-bold text-gray-900">Clés API</h1> actions={
<p className="mt-1 text-sm text-gray-500">
Gérez les clés d&apos;accès programmatique à l&apos;API Xpeditis.
</p>
</div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
disabled={activeKeys.length >= 20} disabled={activeKeys.length >= 20}
@ -392,7 +389,8 @@ export default function ApiKeysPage() {
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Nouvelle clé Nouvelle clé
</button> </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,6 +14,7 @@ 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;
@ -279,13 +280,11 @@ export default function UsersManagementPage() {
</div> </div>
)} )}
{/* Header */} <PageHeader
<div className="flex items-center justify-between"> title="Gestion des Utilisateurs"
<div> description="Gérez les membres de l'équipe et leurs permissions"
<h1 className="text-2xl font-bold text-gray-900">Gestion des Utilisateurs</h1> actions={
<p className="text-sm text-gray-500 mt-1">Gérez les membres de l'équipe et leurs permissions</p> <>
</div>
<div className="flex items-center space-x-3">
<ExportButton <ExportButton
data={allUsers} data={allUsers}
filename="utilisateurs" filename="utilisateurs"
@ -301,22 +300,25 @@ export default function UsersManagementPage() {
{licenseStatus?.canInvite ? ( {licenseStatus?.canInvite ? (
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
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" 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"
> >
<span className="mr-2">+</span> <span className="mr-1.5">+</span>
Inviter un utilisateur <span className="hidden sm:inline">Inviter un utilisateur</span>
<span className="sm:hidden">Inviter</span>
</button> </button>
) : ( ) : (
<Link <Link
href="/dashboard/settings/subscription" 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" 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"
> >
<span className="mr-2">+</span> <span className="mr-1.5">+</span>
Mettre à niveau <span className="hidden sm:inline">Mettre à niveau</span>
<span className="sm:hidden">Upgrade</span>
</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-6 lg:px-8"> <div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20">
<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-4 py-2 rounded-full mb-8 border border-white/20" 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"
> >
<Ship className="w-5 h-5 text-brand-turquoise" /> <Ship className="w-4 h-4 sm:w-5 sm:h-5 text-brand-turquoise flex-shrink-0" />
<span className="text-white/90 text-sm font-medium"> <span className="text-white/90 text-xs sm: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-5xl lg:text-7xl font-bold text-white mb-6 leading-tight" className="text-3xl sm:text-5xl lg:text-7xl font-bold text-white mb-4 sm: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-xl lg:text-2xl text-white/80 mb-12 max-w-3xl mx-auto leading-relaxed" 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"
> >
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 space-y-4 sm:space-y-0 sm:space-x-6 mb-12" className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 mb-12 px-4 sm:px-0"
> >
{isAuthenticated && user ? ( {isAuthenticated && user ? (
<Link <Link
href="/dashboard" href="/dashboard"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
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" 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"
> >
<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-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" 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"
> >
<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-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" 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"
> >
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-16 bg-gray-50"> <section ref={statsRef} className="py-10 sm: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-6 lg:px-8" className="max-w-7xl mx-auto px-4 sm: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-5xl lg:text-6xl font-bold text-brand-navy mb-2 tabular-nums" className="text-3xl sm: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-20 lg:py-32"> <section ref={featuresRef} id="features" className="py-12 sm:py-20 lg:py-32">
<div className="max-w-7xl mx-auto px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm: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-16" className="text-center mb-10 sm:mb-16"
> >
<h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4"> <h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
Pourquoi choisir Xpeditis ? Pourquoi choisir Xpeditis ?
</h2> </h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-base sm: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-20 lg:py-32 bg-gradient-to-b from-white to-gray-50" className="py-12 sm:py-20 lg:py-32 bg-gradient-to-b from-white to-gray-50"
> >
<div className="max-w-7xl mx-auto px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm: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-12" className="text-center mb-8 sm: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-4xl lg:text-5xl font-bold text-brand-navy mb-4"> <h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
Des plans adaptés à votre activité Des plans adaptés à votre activité
</h2> </h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-base sm: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-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white" className="py-12 sm:py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white"
> >
<div className="max-w-7xl mx-auto px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm: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-16" className="text-center mb-10 sm:mb-16"
> >
<h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4"> <h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-3 sm:mb-4">
Ils nous font confiance Ils nous font confiance
</h2> </h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> <p className="text-base sm: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-20 lg:py-32"> <section ref={ctaRef} className="py-12 sm: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-6 lg:px-8 text-center" className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"
> >
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-6"> <h2 className="text-2xl sm:text-4xl lg:text-5xl font-bold text-brand-navy mb-4 sm:mb-6">
Prêt à simplifier votre fret maritime ? Prêt à simplifier votre fret maritime ?
</h2> </h2>
<p className="text-xl text-gray-600 mb-10"> <p className="text-base sm:text-xl text-gray-600 mb-8 sm: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 space-y-4 sm:space-y-0 sm:space-x-6" className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-6 px-2 sm:px-0"
> >
{isAuthenticated && user ? ( {isAuthenticated && user ? (
<Link <Link
href="/dashboard" href="/dashboard"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
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" 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"
> >
<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-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" 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"
> >
<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-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" 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"
> >
Se connecter Se connecter
</Link> </Link>
@ -1070,7 +1070,7 @@ export default function LandingPage() {
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
className="flex items-center justify-center space-x-6 mt-8 text-sm text-gray-500" className="flex flex-wrap items-center justify-center gap-4 sm:gap-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,6 +11,7 @@ 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 {
@ -27,6 +28,12 @@ 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(() => {
@ -53,7 +60,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 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" 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}`}
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" />
@ -69,7 +76,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 bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-2xl" className={`fixed left-0 right-0 z-50 bg-white border-t border-gray-200 shadow-2xl ${mobileOffset}`}
> >
<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,8 +10,11 @@ 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;
@ -21,7 +24,14 @@ 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' },
@ -66,6 +76,7 @@ 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 }}
@ -73,18 +84,28 @@ 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-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 lg:px-8">
<div className="flex items-center justify-between h-20"> <div className="flex items-center justify-between h-16 md: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={70} width={60}
height={80} height={70}
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"
@ -226,5 +247,138 @@ 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

@ -0,0 +1,30 @@
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>
);
}