fix mobile version
This commit is contained in:
parent
be1de882c3
commit
982c893952
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'accès programmatique à l'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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
apps/frontend/src/components/ui/PageHeader.tsx
Normal file
30
apps/frontend/src/components/ui/PageHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user