first commit

This commit is contained in:
David 2025-08-03 02:37:57 +02:00
commit 15d43ee2bd
32 changed files with 7494 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
.env
.idea

60
Dockerfile Normal file
View File

@ -0,0 +1,60 @@
# Multi-stage build pour le frontend React
FROM node:18-alpine AS base
WORKDIR /app
# Installer pnpm
RUN npm install -g pnpm
# Copier les fichiers de dépendances
COPY package.json pnpm-lock.yaml* ./
# Installer les dépendances
RUN pnpm install
# Stage de développement
FROM base AS development
# Copier le code source
COPY . .
# Exposer le port de développement
EXPOSE 3000
# Commande de développement avec hot reload
CMD ["pnpm", "dev", "--host", "0.0.0.0"]
# Stage de build pour la production
FROM base AS build
# Copier le code source
COPY . .
# Variables d'environnement pour le build
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
# Construire l'application
RUN pnpm build
# Stage de production avec Nginx
FROM nginx:alpine AS production
# Copier la configuration Nginx personnalisée
COPY infrastructure/nginx/frontend.conf /etc/nginx/conf.d/default.conf
# Copier les fichiers buildés
COPY --from=build /app/dist /usr/share/nginx/html
# Copier le script d'entrée pour les variables d'environnement
COPY infrastructure/docker/frontend-entrypoint.sh /docker-entrypoint.d/40-frontend-config.sh
RUN chmod +x /docker-entrypoint.d/40-frontend-config.sh
# Exposer le port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# Nginx démarre automatiquement

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

46
index.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0066CC" />
<meta
name="description"
content="Xpeditis - Plateforme SaaS pour le transport maritime international. Devis instantané, suivi en temps réel, gestion des expéditions. Simplifiez votre logistique maritime."
/>
<meta
name="keywords"
content="transport maritime, logistique, SaaS, expédition, fret maritime, devis transport, suivi cargaison, commerce international, Xpeditis"
/>
<meta name="author" content="Xpeditis" />
<!-- Open Graph pour un meilleur affichage sur les réseaux sociaux -->
<meta property="og:title" content="Xpeditis - Plateforme Maritime SaaS" />
<meta property="og:description" content="Révolutionnez votre transport maritime avec notre plateforme SaaS. Devis instantané, suivi en temps réel et gestion simplifiée." />
<meta property="og:image" content="/vite.svg" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.xpeditis.com" />
<link rel="apple-touch-icon" href="/vite.svg" />
<link rel="manifest" href="/manifest.json" />
<title>Xpeditis - Plateforme Maritime SaaS | Transport & Logistique</title>
</head>
<body>
<noscript>Vous devez activer JavaScript pour utiliser cette application.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

25
manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "xpeditis-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^3.3.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.8.4",
"axios": "^1.6.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"lucide-react": "^0.294.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-router-dom": "^6.20.1",
"tailwind-merge": "^2.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

3818
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

41
src/App.tsx Normal file
View File

@ -0,0 +1,41 @@
import { Routes, Route } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import ProtectedRoute from './components/auth/ProtectedRoute'
import Layout from './components/layout/Layout'
import LandingPage from './pages/LandingPage'
import HomePage from './pages/HomePage'
import QuotePage from './pages/QuotePage'
import BookingPage from './pages/BookingPage'
import TrackingPage from './pages/TrackingPage'
import DashboardPage from './pages/DashboardPage'
import ShipSchedulePage from './pages/ShipSchedulePage'
import LoginPage from './pages/LoginPage'
function App() {
return (
<AuthProvider>
<Routes>
{/* Landing page publique */}
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
{/* Application avec layout (dashboard) - Routes protégées */}
<Route path="/app" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<DashboardPage />} />
<Route path="home" element={<HomePage />} />
<Route path="quote" element={<QuotePage />} />
<Route path="bookings" element={<BookingPage />} />
<Route path="tracking" element={<TrackingPage />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="schedule" element={<ShipSchedulePage />} />
</Route>
</Routes>
</AuthProvider>
)
}
export default App

View File

@ -0,0 +1,38 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { Ship } from 'lucide-react';
interface ProtectedRouteProps {
children: React.ReactNode;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
// Afficher un loader pendant la vérification de l'authentification
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<div className="flex items-center justify-center mb-2">
<Ship className="h-6 w-6 text-blue-600 mr-2" />
<span className="text-lg font-semibold text-gray-900">Xpeditis</span>
</div>
<p className="text-gray-600">Vérification de l'authentification...</p>
</div>
</div>
);
}
// Si pas authentifié, rediriger vers la page de connexion
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// Si authentifié, afficher le contenu protégé
return <>{children}</>;
};
export default ProtectedRoute;

View File

@ -0,0 +1,340 @@
import { Link, useLocation, Outlet } from 'react-router-dom';
import { Ship, Menu, X, Home, FileText, Package, MapPin, BarChart3, User, LogOut, Bell, Settings, Globe } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import {FC, ReactNode, useEffect, useState} from "react";
interface LayoutProps {
children?: ReactNode;
}
const Layout: FC<LayoutProps> = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
const [isLanguageOpen, setIsLanguageOpen] = useState(false);
const [currentLanguage, setCurrentLanguage] = useState('fr');
const location = useLocation();
const { user, logout } = useAuth();
const navigation = [
{ name: 'Accueil', href: '/app/home', icon: Home },
{ name: 'Tableau de bord', href: '/app', icon: BarChart3 },
{ name: 'Devis', href: '/app/quote', icon: FileText },
{ name: 'Réservations', href: '/app/bookings', icon: Package },
{ name: 'Suivi', href: '/app/tracking', icon: MapPin }
];
const isActive = (path: string) => location.pathname === path;
// Fermer le menu utilisateur quand on clique ailleurs
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isUserMenuOpen) {
setIsUserMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isUserMenuOpen]);
return (
<div className="min-h-screen bg-gray-50">
{/* Header fixe en haut */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white border-b border-gray-200 shadow-sm">
<div className="flex items-center justify-between h-16 px-6">
{/* Logo à gauche */}
<Link to="/" className="flex items-center">
<Ship className="h-8 w-8 text-blue-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Xpeditis</span>
</Link>
{/* Actions et User Menu tout à droite */}
<div className="flex items-center space-x-2">
{/* Centre de notifications */}
<div className="relative">
<button
onClick={() => setIsNotificationOpen(!isNotificationOpen)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors relative"
>
<Bell className="h-5 w-5 text-gray-600" />
{/* Badge de notification */}
<span className="absolute -top-1 -right-1 h-4 w-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
3
</span>
</button>
{/* Dropdown Notifications */}
{isNotificationOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
<div className="px-4 py-2 text-sm font-medium text-gray-900 border-b border-gray-100">
Notifications
</div>
<div className="max-h-64 overflow-y-auto">
<div className="px-4 py-3 hover:bg-gray-50 border-b border-gray-100">
<div className="text-sm font-medium text-gray-900">Nouvelle expédition</div>
<div className="text-xs text-gray-500">Votre expédition XPD-2025-003 a é confirmée</div>
<div className="text-xs text-gray-400 mt-1">Il y a 2 heures</div>
</div>
<div className="px-4 py-3 hover:bg-gray-50 border-b border-gray-100">
<div className="text-sm font-medium text-gray-900">Devis approuvé</div>
<div className="text-xs text-gray-500">Le devis QT-2025-012 a é accepté</div>
<div className="text-xs text-gray-400 mt-1">Il y a 4 heures</div>
</div>
<div className="px-4 py-3 hover:bg-gray-50">
<div className="text-sm font-medium text-gray-900">Livraison terminée</div>
<div className="text-xs text-gray-500">L'expédition XPD-2025-001 a é livrée</div>
<div className="text-xs text-gray-400 mt-1">Il y a 1 jour</div>
</div>
</div>
<div className="px-4 py-2 border-t border-gray-100">
<button className="text-sm text-blue-600 hover:text-blue-800">Voir toutes les notifications</button>
</div>
</div>
)}
</div>
{/* Paramètres */}
<button className="p-2 rounded-lg hover:bg-gray-100 transition-colors">
<Settings className="h-5 w-5 text-gray-600" />
</button>
{/* Sélecteur de langue */}
<div className="relative">
<button
onClick={() => setIsLanguageOpen(!isLanguageOpen)}
className="flex items-center space-x-1 px-2 py-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<Globe className="h-4 w-4 text-gray-600" />
<span className="text-sm font-medium text-gray-700 uppercase">{currentLanguage}</span>
</button>
{/* Dropdown Langue */}
{isLanguageOpen && (
<div className="absolute right-0 mt-2 w-32 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
<button
onClick={() => {
setCurrentLanguage('fr');
setIsLanguageOpen(false);
}}
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 transition-colors ${
currentLanguage === 'fr' ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
}`}
>
🇫🇷 Français
</button>
<button
onClick={() => {
setCurrentLanguage('en');
setIsLanguageOpen(false);
}}
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 transition-colors ${
currentLanguage === 'en' ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
}`}
>
🇬🇧 English
</button>
</div>
)}
</div>
{/* User Menu */}
<div className="relative">
<button
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
className="flex items-center space-x-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
>
<User className="h-5 w-5 text-gray-600" />
<span className="text-sm font-medium text-gray-700">{user?.name || user?.email}</span>
</button>
{/* Dropdown Menu */}
{isUserMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
<div className="px-4 py-2 text-sm text-gray-700 border-b border-gray-100">
<div className="font-medium">{user?.name || 'Utilisateur'}</div>
<div className="text-gray-500">{user?.email}</div>
</div>
<button
onClick={() => {
logout();
setIsUserMenuOpen(false);
}}
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
Déconnexion
</button>
</div>
)}
</div>
</div>
</div>
</header>
{/* Contenu principal avec sidebar */}
<div className="flex pt-16 min-h-screen"> {/* pt-16 pour compenser le header fixe */}
{/* Sidebar rétractable */}
<div className="hidden md:flex md:flex-col group justify-center" style={{height: 'calc(100vh - 4rem)'}}>
<div className="flex flex-col bg-white border border-gray-200 rounded-lg shadow-sm transition-all duration-300 ease-in-out w-16 group-hover:w-64 overflow-hidden">
{/* Navigation */}
<nav className="py-4 px-2 space-y-1">
{navigation.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.name}
to={item.href}
className={`group/item flex items-center px-2 py-3 text-sm font-medium rounded-md transition-all duration-200 relative ${
isActive(item.href)
? 'bg-blue-100 text-blue-900'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
title={item.name}
>
<Icon className={`flex-shrink-0 h-6 w-6 transition-colors ${
isActive(item.href) ? 'text-blue-500' : 'text-gray-400 group-hover/item:text-gray-500'
}`} />
<span className={`ml-3 whitespace-nowrap transition-all duration-300 opacity-0 group-hover:opacity-100 group-hover:translate-x-0 translate-x-2 ${
isActive(item.href) ? 'text-blue-900' : 'text-gray-600 group-hover/item:text-gray-900'
}`}>
{item.name}
</span>
</Link>
);
})}
</nav>
</div>
</div>
{/* Zone de contenu principal */}
<div className="flex-1 flex flex-col overflow-hidden transition-all duration-300">
<main className="flex-1 relative overflow-y-auto focus:outline-none">
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<Outlet />
</div>
</div>
</main>
</div>
</div>
{/* Header mobile */}
<div className="md:hidden">
<div className="fixed top-0 left-0 right-0 z-40 bg-white shadow-sm border-b border-gray-200">
<div className="flex justify-between items-center h-16 px-4">
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
>
{isSidebarOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
<Link to="/" className="flex items-center">
<Ship className="h-8 w-8 text-blue-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Xpeditis</span>
</Link>
{/* User menu mobile */}
<div className="relative">
<button
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
className="flex items-center p-2 rounded-full hover:bg-gray-100 transition-colors"
>
<User className="h-5 w-5 text-gray-600" />
</button>
{/* Dropdown Menu Mobile */}
{isUserMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
<div className="px-4 py-2 text-sm text-gray-700 border-b border-gray-100">
<div className="font-medium">{user?.name || 'Utilisateur'}</div>
<div className="text-gray-500">{user?.email}</div>
</div>
<button
onClick={() => {
logout();
setIsUserMenuOpen(false);
}}
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
>
<LogOut className="h-4 w-4 mr-2" />
Déconnexion
</button>
</div>
)}
</div>
</div>
</div>
</div>
{/* Mobile Sidebar */}
{isSidebarOpen && (
<div className="md:hidden">
<div className="fixed inset-0 z-40 flex">
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setIsSidebarOpen(false)} />
<div className="relative flex-1 flex flex-col max-w-xs w-full bg-white">
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
onClick={() => setIsSidebarOpen(false)}
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
>
<X className="h-6 w-6 text-white" />
</button>
</div>
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<nav className="mt-5 px-2 space-y-1">
{navigation.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.name}
to={item.href}
onClick={() => setIsSidebarOpen(false)}
className={`group flex items-center px-2 py-2 text-base font-medium rounded-md ${
isActive(item.href)
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<Icon className="mr-4 h-6 w-6" />
{item.name}
</Link>
);
})}
</nav>
</div>
</div>
</div>
</div>
)}
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center">
<div className="flex items-center">
<Ship className="h-6 w-6 text-blue-600" />
<span className="ml-2 text-sm text-gray-600">
© 2025 Xpeditis. Plateforme maritime SaaS.
</span>
</div>
<div className="flex space-x-6">
<a href="#" className="text-sm text-gray-600 hover:text-gray-900">
Support
</a>
<a href="#" className="text-sm text-gray-600 hover:text-gray-900">
Documentation
</a>
<a href="#" className="text-sm text-gray-600 hover:text-gray-900">
Contact
</a>
</div>
</div>
</div>
</footer>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,10 @@
function QuoteForm({ customerId }) {
return (
<div className="space-y-6">
</div>
)
}

View File

@ -0,0 +1,19 @@
import {QuoteResponse} from "../../lib/api";
interface QuoteResultsProps {
quotes: QuoteResponse[]
}
export default function QuoteResults({ quotes }: QuoteResultsProps) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Offres disponibles</h3>
</div>
</div>
)
}

127
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import {cn} from "../../lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,28 @@
import {useToast} from "../../hooks/use-toast";
import {Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport} from "./toast";
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,78 @@
import { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: string;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Vérifier si l'utilisateur est déjà connecté au chargement
useEffect(() => {
const savedUser = localStorage.getItem('xpeditis_user');
if (savedUser) {
try {
setUser(JSON.parse(savedUser));
} catch (error) {
localStorage.removeItem('xpeditis_user');
}
}
setIsLoading(false);
}, []);
const login = async (email: string, password: string): Promise<boolean> => {
// Simulation d'authentification
if (email === 'demo@xpeditis.com' && password === 'demo123') {
const userData: User = {
id: '1',
email: 'demo@xpeditis.com',
name: 'Utilisateur Demo'
};
setUser(userData);
localStorage.setItem('xpeditis_user', JSON.stringify(userData));
return true;
}
return false;
};
const logout = () => {
setUser(null);
localStorage.removeItem('xpeditis_user');
};
const value = {
user,
isAuthenticated: !!user,
isLoading,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

186
src/hooks/use-toast.ts Normal file
View File

@ -0,0 +1,186 @@
import * as React from "react"
import {ToastActionElement, ToastProps} from "../components/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

86
src/index.css Normal file
View File

@ -0,0 +1,86 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Styles spécifiques pour le thème maritime */
.maritime-gradient {
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 50%, #0369a1 100%);
}
.maritime-card {
@apply bg-white/95 backdrop-blur-sm border border-blue-100 shadow-lg;
}
.maritime-button {
@apply bg-blue-600 hover:bg-blue-700 text-white font-medium px-6 py-2 rounded-lg transition-colors;
}
.maritime-input {
@apply border-blue-200 focus:border-blue-500 focus:ring-blue-500/20;
}

120
src/lib/api.ts Normal file
View File

@ -0,0 +1,120 @@
import axios from 'axios'
const API_BASE_URL = process.env.VITE_API_URL || '/api'
export const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor pour ajouter le token JWT
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor pour gérer les erreurs
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
// Types pour les API calls
export interface QuoteRequest {
originPort: string
destinationPort: string
originCountry?: string
destinationCountry?: string
transportType: 'MARITIME' | 'ROAD' | 'MULTIMODAL'
containerType: 'FCL_20' | 'FCL_40' | 'FCL_40HC' | 'LCL'
quantity: number
totalWeight?: number
totalVolume?: number
customerId?: string
cargoItems?: CargoItem[]
additionalServiceCodes?: string[]
isHazardous?: boolean
requiresInsurance?: boolean
}
export interface CargoItem {
description: string
quantity: number
weight: number
volume: number
hsCode?: string
value?: number
currency?: string
isHazardous?: boolean
}
export interface QuoteResponse {
id: string
originPort: string
destinationPort: string
originCountry?: string
destinationCountry?: string
transportType: string
serviceType: 'EXPRESS' | 'STANDARD' | 'ECO'
containerType: string
quantity: number
totalWeight?: number
totalVolume?: number
price: number
currency: string
transitDays: number
validUntil: string
createdAt: string
status: string
customerId?: string
additionalServices?: AdditionalService[]
}
export interface AdditionalService {
code: string
name: string
description: string
price: number
currency: string
mandatory: boolean
}
// API functions
export const quoteApi = {
generateInstantQuote: (request: QuoteRequest): Promise<QuoteResponse[]> =>
api.post('/quotes/instant', request).then(res => res.data),
getQuoteById: (quoteId: string): Promise<QuoteResponse> =>
api.get(`/quotes/${quoteId}`).then(res => res.data),
getQuotesByCustomer: (customerId: string): Promise<QuoteResponse[]> =>
api.get(`/quotes/customer/${customerId}`).then(res => res.data),
acceptQuote: (quoteId: string, customerId: string): Promise<QuoteResponse> =>
api.post(`/quotes/${quoteId}/accept?customerId=${customerId}`).then(res => res.data),
rejectQuote: (quoteId: string, customerId: string): Promise<QuoteResponse> =>
api.post(`/quotes/${quoteId}/reject?customerId=${customerId}`).then(res => res.data),
isQuoteValid: (quoteId: string): Promise<boolean> =>
api.get(`/quotes/${quoteId}/valid`).then(res => res.data),
}
export default api

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

25
src/main.tsx Normal file
View File

@ -0,0 +1,25 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.tsx'
import './index.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
)

219
src/pages/BookingPage.tsx Normal file
View File

@ -0,0 +1,219 @@
import { useState } from 'react';
import { Package, MapPin, Calendar, Clock, Eye, Download } from 'lucide-react';
interface Booking {
id: string;
reference: string;
origin: string;
destination: string;
departureDate: string;
arrivalDate: string;
status: 'confirmed' | 'in-transit' | 'delivered' | 'pending';
service: string;
price: number;
cargoType: string;
}
const BookingPage: React.FC = () => {
const [bookings] = useState<Booking[]>([
{
id: '1',
reference: 'XPD-2025-001',
origin: 'Le Havre, France',
destination: 'Shanghai, Chine',
departureDate: '2025-01-15',
arrivalDate: '2025-02-05',
status: 'in-transit',
service: 'Standard',
price: 1950,
cargoType: 'Conteneur FCL',
},
{
id: '2',
reference: 'XPD-2025-002',
origin: 'Marseille, France',
destination: 'New York, États-Unis',
departureDate: '2025-01-20',
arrivalDate: '2025-02-12',
status: 'confirmed',
service: 'Express',
price: 2850,
cargoType: 'Marchandise générale',
},
{
id: '3',
reference: 'XPD-2024-156',
origin: 'Rotterdam, Pays-Bas',
destination: 'Hamburg, Allemagne',
departureDate: '2024-12-10',
arrivalDate: '2024-12-15',
status: 'delivered',
service: 'Économique',
price: 1450,
cargoType: 'Groupage LCL',
},
]);
const getStatusColor = (status: string) => {
switch (status) {
case 'confirmed':
return 'bg-blue-100 text-blue-800';
case 'in-transit':
return 'bg-yellow-100 text-yellow-800';
case 'delivered':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'confirmed':
return 'Confirmé';
case 'in-transit':
return 'En transit';
case 'delivered':
return 'Livré';
case 'pending':
return 'En attente';
default:
return status;
}
};
return (
<div className="space-y-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Mes réservations</h1>
<p className="text-gray-600 mt-2">
Gérez et suivez toutes vos expéditions maritimes
</p>
</div>
<button className="maritime-button">
Nouvelle réservation
</button>
</div>
{/* Statistiques rapides */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-blue-600 mb-2">3</div>
<div className="text-sm text-gray-600">Réservations actives</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-yellow-600 mb-2">1</div>
<div className="text-sm text-gray-600">En transit</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-green-600 mb-2">1</div>
<div className="text-sm text-gray-600">Livré ce mois</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-gray-900 mb-2">6,250</div>
<div className="text-sm text-gray-600">Total expédié</div>
</div>
</div>
{/* Liste des réservations */}
<div className="space-y-4">
{bookings.map((booking) => (
<div key={booking.id} className="maritime-card p-6 hover:shadow-lg transition-shadow">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
{/* Informations principales */}
<div className="flex-1">
<div className="flex items-center space-x-4 mb-3">
<h3 className="text-lg font-semibold text-gray-900">
{booking.reference}
</h3>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(booking.status)}`}>
{getStatusText(booking.status)}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div className="flex items-center">
<MapPin className="h-4 w-4 mr-2 text-blue-600" />
<span>{booking.origin} {booking.destination}</span>
</div>
<div className="flex items-center">
<Package className="h-4 w-4 mr-2 text-blue-600" />
<span>{booking.cargoType} - {booking.service}</span>
</div>
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-2 text-blue-600" />
<span>Départ: {new Date(booking.departureDate).toLocaleDateString('fr-FR')}</span>
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-blue-600" />
<span>Arrivée: {new Date(booking.arrivalDate).toLocaleDateString('fr-FR')}</span>
</div>
</div>
</div>
{/* Prix et actions */}
<div className="flex flex-col lg:flex-row items-start lg:items-center space-y-3 lg:space-y-0 lg:space-x-6">
<div className="text-right">
<div className="text-2xl font-bold text-gray-900">
{booking.price.toLocaleString()}
</div>
<div className="text-sm text-gray-500">Prix total</div>
</div>
<div className="flex space-x-2">
<button className="flex items-center px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<Eye className="h-4 w-4 mr-1" />
Détails
</button>
<button className="flex items-center px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<Download className="h-4 w-4 mr-1" />
Documents
</button>
</div>
</div>
</div>
{/* Barre de progression pour les expéditions en transit */}
{booking.status === 'in-transit' && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Progression de l'expédition</span>
<span>65%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '65%' }}></div>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>Départ</span>
<span>En mer</span>
<span>Arrivée</span>
</div>
</div>
)}
</div>
))}
</div>
{/* Message si aucune réservation */}
{bookings.length === 0 && (
<div className="text-center py-12">
<Package className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Aucune réservation
</h3>
<p className="text-gray-600 mb-6">
Vous n'avez pas encore de réservation. Commencez par demander un devis.
</p>
<button className="maritime-button">
Demander un devis
</button>
</div>
)}
</div>
);
};
export default BookingPage;

260
src/pages/DashboardPage.tsx Normal file
View File

@ -0,0 +1,260 @@
import { BarChart3, TrendingUp, Ship, Package, DollarSign, Clock, MapPin, AlertTriangle } from 'lucide-react';
const DashboardPage: React.FC = () => {
const stats = [
{
name: 'Expéditions ce mois',
value: '24',
change: '+12%',
changeType: 'positive',
icon: Ship,
},
{
name: 'Chiffre d\'affaires',
value: '45,230€',
change: '+8.2%',
changeType: 'positive',
icon: DollarSign,
},
{
name: 'Temps moyen de transit',
value: '18.5j',
change: '-2.1j',
changeType: 'positive',
icon: Clock,
},
{
name: 'Taux de livraison',
value: '98.2%',
change: '+0.8%',
changeType: 'positive',
icon: Package,
},
];
const recentShipments = [
{
id: 'XPD-2025-001',
origin: 'Le Havre',
destination: 'Shanghai',
status: 'En transit',
progress: 65,
eta: '2025-02-05',
},
{
id: 'XPD-2025-002',
origin: 'Marseille',
destination: 'New York',
status: 'Confirmé',
progress: 0,
eta: '2025-02-12',
},
{
id: 'XPD-2025-003',
origin: 'Rotterdam',
destination: 'Hamburg',
status: 'Livré',
progress: 100,
eta: '2024-12-15',
},
];
const alerts = [
{
id: '1',
type: 'warning',
message: 'Retard possible sur XPD-2025-001 dû aux conditions météo',
time: 'Il y a 2h',
},
{
id: '2',
type: 'info',
message: 'Nouveau devis disponible pour la route Le Havre-Singapore',
time: 'Il y a 4h',
},
];
const getStatusColor = (status: string) => {
switch (status) {
case 'Confirmé':
return 'bg-blue-100 text-blue-800';
case 'En transit':
return 'bg-yellow-100 text-yellow-800';
case 'Livré':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Tableau de bord</h1>
<p className="text-gray-600 mt-2">
Vue d'ensemble de vos activités maritimes
</p>
</div>
{/* Statistiques principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<div key={stat.name} className="maritime-card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">{stat.name}</p>
<p className="text-2xl font-bold text-gray-900 mt-1">
{stat.value}
</p>
</div>
<Icon className="h-8 w-8 text-blue-600" />
</div>
<div className="mt-4 flex items-center">
<TrendingUp className={`h-4 w-4 mr-1 ${
stat.changeType === 'positive' ? 'text-green-600' : 'text-red-600'
}`} />
<span className={`text-sm font-medium ${
stat.changeType === 'positive' ? 'text-green-600' : 'text-red-600'
}`}>
{stat.change}
</span>
<span className="text-sm text-gray-600 ml-1">vs mois dernier</span>
</div>
</div>
);
})}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Expéditions récentes */}
<div className="lg:col-span-2">
<div className="maritime-card p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">
Expéditions récentes
</h2>
<button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
Voir tout
</button>
</div>
<div className="space-y-4">
{recentShipments.map((shipment) => (
<div key={shipment.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<h3 className="font-medium text-gray-900">{shipment.id}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(shipment.status)}`}>
{shipment.status}
</span>
</div>
<div className="text-sm text-gray-600">
ETA: {new Date(shipment.eta).toLocaleDateString('fr-FR')}
</div>
</div>
<div className="flex items-center text-sm text-gray-600 mb-3">
<MapPin className="h-4 w-4 mr-1" />
<span>{shipment.origin} {shipment.destination}</span>
</div>
{shipment.status === 'En transit' && (
<div>
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>Progression</span>
<span>{shipment.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${shipment.progress}%` }}
></div>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* Alertes et notifications */}
<div className="space-y-6">
<div className="maritime-card p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Alertes récentes
</h2>
<div className="space-y-3">
{alerts.map((alert) => (
<div key={alert.id} className="flex items-start space-x-3 p-3 bg-yellow-50 rounded-lg">
<AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5" />
<div className="flex-1">
<p className="text-sm text-gray-900">{alert.message}</p>
<p className="text-xs text-gray-600 mt-1">{alert.time}</p>
</div>
</div>
))}
</div>
</div>
{/* Actions rapides */}
<div className="maritime-card p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Actions rapides
</h2>
<div className="space-y-3">
<button className="w-full maritime-button text-left">
Nouvelle expédition
</button>
<button className="w-full border border-gray-300 text-gray-700 hover:bg-gray-50 p-3 rounded-lg font-medium transition-colors text-left">
Demander un devis
</button>
<button className="w-full border border-gray-300 text-gray-700 hover:bg-gray-50 p-3 rounded-lg font-medium transition-colors text-left">
Suivre une expédition
</button>
</div>
</div>
{/* Graphique des performances */}
<div className="maritime-card p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Performances mensuelles
</h2>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Janvier</span>
<div className="flex items-center space-x-2">
<div className="w-24 bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '85%' }}></div>
</div>
<span className="text-sm font-medium">85%</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Décembre</span>
<div className="flex items-center space-x-2">
<div className="w-24 bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '92%' }}></div>
</div>
<span className="text-sm font-medium">92%</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Novembre</span>
<div className="flex items-center space-x-2">
<div className="w-24 bg-gray-200 rounded-full h-2">
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '78%' }}></div>
</div>
<span className="text-sm font-medium">78%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default DashboardPage;

157
src/pages/HomePage.tsx Normal file
View File

@ -0,0 +1,157 @@
import { Link } from 'react-router-dom';
import { Ship, FileText, MapPin, BarChart3, Clock, Globe, Shield, Zap } from 'lucide-react';
const HomePage: React.FC = () => {
const stats = [
{ name: 'Expéditions traitées', value: '12,543', icon: Ship },
{ name: 'Devis générés', value: '8,921', icon: FileText },
{ name: 'Ports desservis', value: '450+', icon: Globe },
{ name: 'Clients satisfaits', value: '2,100+', icon: BarChart3 },
];
const features = [
{
name: 'Devis instantané',
description: 'Obtenez un devis en quelques secondes pour vos expéditions maritimes.',
icon: Zap,
href: '/quote',
},
{
name: 'Suivi en temps réel',
description: 'Suivez vos expéditions en temps réel avec notre système de tracking avancé.',
icon: MapPin,
href: '/tracking',
},
{
name: 'Gestion simplifiée',
description: 'Gérez toutes vos expéditions depuis un tableau de bord unifié.',
icon: BarChart3,
href: '/dashboard',
},
{
name: 'Sécurité garantie',
description: 'Vos données et expéditions sont protégées par nos systèmes sécurisés.',
icon: Shield,
href: '#',
},
];
return (
<div className="space-y-12">
{/* Hero Section */}
<div className="relative overflow-hidden">
<div className="maritime-gradient rounded-2xl px-8 py-16 text-center text-white">
<h1 className="text-4xl md:text-6xl font-bold mb-6">
Votre plateforme maritime
<span className="block text-blue-200">nouvelle génération</span>
</h1>
<p className="text-xl md:text-2xl mb-8 text-blue-100 max-w-3xl mx-auto">
Simplifiez vos expéditions maritimes avec notre solution SaaS complète :
devis instantané, suivi en temps réel et gestion centralisée.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
to="/quote"
className="bg-white text-blue-600 hover:bg-blue-50 px-8 py-4 rounded-lg font-semibold text-lg transition-colors inline-flex items-center justify-center"
>
<FileText className="h-5 w-5 mr-2" />
Demander un devis
</Link>
<Link
to="/tracking"
className="border-2 border-white text-white hover:bg-white hover:text-blue-600 px-8 py-4 rounded-lg font-semibold text-lg transition-colors inline-flex items-center justify-center"
>
<MapPin className="h-5 w-5 mr-2" />
Suivre une expédition
</Link>
</div>
</div>
</div>
{/* Stats Section */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<div key={stat.name} className="maritime-card p-6 text-center">
<Icon className="h-8 w-8 text-blue-600 mx-auto mb-4" />
<div className="text-3xl font-bold text-gray-900 mb-2">{stat.value}</div>
<div className="text-gray-600">{stat.name}</div>
</div>
);
})}
</div>
{/* Features Section */}
<div>
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Pourquoi choisir Xpeditis ?
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Notre plateforme vous offre tous les outils nécessaires pour optimiser
vos opérations de transport maritime.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{features.map((feature) => {
const Icon = feature.icon;
return (
<div key={feature.name} className="maritime-card p-8 hover:shadow-xl transition-shadow">
<div className="flex items-start">
<div className="flex-shrink-0">
<Icon className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-6">
<h3 className="text-xl font-semibold text-gray-900 mb-3">
{feature.name}
</h3>
<p className="text-gray-600 mb-4">
{feature.description}
</p>
{feature.href !== '#' && (
<Link
to={feature.href}
className="text-blue-600 hover:text-blue-700 font-medium inline-flex items-center"
>
En savoir plus
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
{/* CTA Section */}
<div className="maritime-card p-8 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Prêt à optimiser vos expéditions ?
</h2>
<p className="text-gray-600 mb-6 max-w-2xl mx-auto">
Rejoignez plus de 2,100 entreprises qui font confiance à Xpeditis
pour leurs opérations de transport maritime.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
to="/quote"
className="maritime-button"
>
Commencer maintenant
</Link>
<button className="border border-gray-300 text-gray-700 hover:bg-gray-50 px-6 py-2 rounded-lg font-medium transition-colors">
Planifier une démo
</button>
</div>
</div>
</div>
);
};
export default HomePage;

459
src/pages/LandingPage.tsx Normal file
View File

@ -0,0 +1,459 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Ship,
Globe,
Clock,
Shield,
BarChart3,
Users,
MapPin,
FileText,
ArrowRight,
CheckCircle,
Star,
Phone,
Mail,
MapPin as LocationIcon,
Search,
Calendar,
Package,
Anchor,
TrendingUp,
Award,
Truck
} from 'lucide-react';
const LandingPage: React.FC = () => {
const [searchData, setSearchData] = useState({
origin: '',
destination: '',
departureDate: '',
cargoType: 'FCL'
});
const popularRoutes = [
{ from: 'Le Havre', to: 'New York', price: '€2,450', duration: '12-15 jours' },
{ from: 'Marseille', to: 'Shanghai', price: '€1,890', duration: '18-22 jours' },
{ from: 'Rotterdam', to: 'Los Angeles', price: '€2,180', duration: '14-18 jours' },
{ from: 'Hamburg', to: 'Singapore', price: '€1,650', duration: '16-20 jours' }
];
const ports = [
'Le Havre, France', 'Marseille, France', 'Rotterdam, Pays-Bas', 'Hamburg, Allemagne',
'Shanghai, Chine', 'New York, États-Unis', 'Los Angeles, États-Unis', 'Singapore',
'Dubai, EAU', 'Hong Kong', 'Anvers, Belgique', 'Barcelone, Espagne'
];
const stats = [
{ value: '2M+', label: 'Conteneurs expédiés', icon: Ship },
{ value: '140', label: 'Pays desservis', icon: Globe },
{ value: '35%', label: 'Économies moyennes', icon: TrendingUp },
{ value: '450+', label: 'Ports connectés', icon: Anchor }
];
const handleSearch = () => {
// Rediriger vers la page de devis avec les paramètres de recherche
window.location.href = `/app/quote?origin=${searchData.origin}&destination=${searchData.destination}&date=${searchData.departureDate}&type=${searchData.cargoType}`;
};
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<Ship className="h-8 w-8 text-blue-600 mr-2" />
<span className="text-2xl font-bold text-gray-900">Xpeditis</span>
</div>
<nav className="hidden md:flex space-x-8">
<a href="#services" className="text-gray-700 hover:text-blue-600 font-medium">Expéditions</a>
<a href="#tracking" className="text-gray-700 hover:text-blue-600 font-medium">Suivi</a>
<a href="#about" className="text-gray-700 hover:text-blue-600 font-medium">Solutions</a>
<a href="#contact" className="text-gray-700 hover:text-blue-600 font-medium">Support</a>
</nav>
<div className="flex items-center space-x-4">
<Link
to="/login"
className="text-blue-600 hover:text-blue-700 font-medium"
>
Se connecter
</Link>
<Link
to="/app/quote"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
Obtenir un devis
</Link>
</div>
</div>
</div>
</header>
{/* Hero Section avec moteur de recherche */}
<section className="relative bg-gradient-to-br from-blue-50 to-white py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4">
Recherchez et comparez les tarifs de
<span className="text-blue-600"> transport maritime</span>
</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Trouvez les meilleures offres parmi des centaines de transporteurs maritimes.
Comparez les prix, les délais et réservez en quelques clics.
</p>
</div>
{/* Moteur de recherche principal */}
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
<div className="flex flex-wrap items-center gap-4 mb-4">
<button className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg font-medium">
<Ship className="h-4 w-4 mr-2" />
Expédition maritime
</button>
<div className="flex items-center space-x-4">
<label className="flex items-center">
<input
type="radio"
name="cargoType"
value="FCL"
checked={searchData.cargoType === 'FCL'}
onChange={(e) => setSearchData({...searchData, cargoType: e.target.value})}
className="mr-2"
/>
<span className="text-sm font-medium">FCL (Conteneur complet)</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="cargoType"
value="LCL"
checked={searchData.cargoType === 'LCL'}
onChange={(e) => setSearchData({...searchData, cargoType: e.target.value})}
className="mr-2"
/>
<span className="text-sm font-medium">LCL (Groupage)</span>
</label>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">Port d'origine</label>
<select
value={searchData.origin}
onChange={(e) => setSearchData({...searchData, origin: e.target.value})}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Sélectionner un port</option>
{ports.map(port => (
<option key={port} value={port}>{port}</option>
))}
</select>
</div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">Port de destination</label>
<select
value={searchData.destination}
onChange={(e) => setSearchData({...searchData, destination: e.target.value})}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Sélectionner un port</option>
{ports.map(port => (
<option key={port} value={port}>{port}</option>
))}
</select>
</div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">Date de départ</label>
<input
type="date"
value={searchData.departureDate}
onChange={(e) => setSearchData({...searchData, departureDate: e.target.value})}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-end">
<button
onClick={handleSearch}
className="w-full bg-blue-600 text-white p-3 rounded-lg hover:bg-blue-700 transition-colors font-medium flex items-center justify-center"
>
<Search className="h-5 w-5 mr-2" />
Rechercher
</button>
</div>
</div>
</div>
</div>
{/* Routes populaires */}
<div className="mt-12">
<h3 className="text-lg font-semibold text-gray-900 mb-6 text-center">Routes populaires</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{popularRoutes.map((route, index) => (
<div key={index} className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow cursor-pointer">
<div className="flex justify-between items-start mb-2">
<div>
<div className="font-medium text-gray-900">{route.from}</div>
<ArrowRight className="h-4 w-4 text-gray-400 my-1" />
<div className="font-medium text-gray-900">{route.to}</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-blue-600">{route.price}</div>
<div className="text-sm text-gray-500">{route.duration}</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
{stats.map((stat, index) => {
const Icon = stat.icon;
return (
<div key={index} className="text-center">
<Icon className="h-8 w-8 text-blue-600 mx-auto mb-3" />
<div className="text-3xl font-bold text-gray-900 mb-1">{stat.value}</div>
<div className="text-sm text-gray-600">{stat.label}</div>
</div>
);
})}
</div>
</div>
</section>
{/* Services */}
<section id="services" className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Nos solutions de transport maritime
</h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
Des services complets pour tous vos besoins d'expédition internationale
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white rounded-xl border border-gray-200 p-8 hover:shadow-lg transition-shadow">
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg mb-6">
<Ship className="h-6 w-6 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-4">Transport FCL/LCL</h3>
<p className="text-gray-600 mb-6">
Solutions flexibles pour conteneurs complets ou groupage selon vos besoins.
</p>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Conteneurs 20' et 40'
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Groupage optimisé
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Suivi en temps réel
</li>
</ul>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-8 hover:shadow-lg transition-shadow">
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg mb-6">
<Truck className="h-6 w-6 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-4">Logistique Intégrée</h3>
<p className="text-gray-600 mb-6">
Gestion complète de votre chaîne logistique de bout en bout.
</p>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Transport terrestre
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Entreposage sécurisé
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Dédouanement
</li>
</ul>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-8 hover:shadow-lg transition-shadow">
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg mb-6">
<FileText className="h-6 w-6 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-4">Devis Instantané</h3>
<p className="text-gray-600 mb-6">
Obtenez vos tarifs en temps réel et comparez les meilleures offres.
</p>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Prix transparents
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Comparaison automatique
</li>
<li className="flex items-center">
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
Réservation en ligne
</li>
</ul>
</div>
</div>
</div>
</section>
{/* About Section */}
<section id="about" className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-4xl font-bold text-gray-900 mb-6">
21 ans d'expertise maritime
</h2>
<p className="text-lg text-gray-600 mb-6">
Depuis plus de deux décennies, Xpeditis accompagne les entreprises
dans leurs défis logistiques internationaux. Notre expertise et notre
réseau mondial nous permettent d'offrir des solutions sur mesure.
</p>
<div className="grid grid-cols-2 gap-6">
<div>
<div className="text-3xl font-bold text-blue-600 mb-2">99.8%</div>
<div className="text-gray-600">Taux de livraison</div>
</div>
<div>
<div className="text-3xl font-bold text-blue-600 mb-2">24/7</div>
<div className="text-gray-600">Support client</div>
</div>
</div>
</div>
<div>
<img
src="/api/placeholder/500/400"
alt="Port maritime"
className="rounded-lg shadow-lg"
/>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20 bg-blue-600">
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8">
<h2 className="text-4xl font-bold text-white mb-6">
Prêt à expédier avec Xpeditis ?
</h2>
<p className="text-xl text-blue-100 mb-8">
Rejoignez plus de 15,000 entreprises qui nous font confiance pour leurs expéditions maritimes
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
to="/app/quote"
className="bg-white text-blue-600 px-8 py-4 rounded-lg font-semibold text-lg hover:bg-blue-50 transition-colors inline-flex items-center justify-center"
>
Obtenir un devis gratuit
<ArrowRight className="ml-2 h-5 w-5" />
</Link>
<Link
to="/app"
className="border-2 border-white text-white px-8 py-4 rounded-lg font-semibold text-lg hover:bg-white hover:text-blue-600 transition-colors inline-flex items-center justify-center"
>
Accéder au dashboard
</Link>
</div>
</div>
</section>
{/* Footer */}
<footer id="contact" className="bg-gray-900 text-white py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div className="flex items-center mb-4">
<Ship className="h-8 w-8 text-blue-400 mr-2" />
<span className="text-2xl font-bold">Xpeditis</span>
</div>
<p className="text-gray-400 mb-4">
Votre partenaire de confiance pour le transport maritime international.
</p>
<div className="flex space-x-4">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
<span className="text-xs">f</span>
</div>
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
<span className="text-xs">in</span>
</div>
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
<span className="text-xs">tw</span>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Services</h3>
<ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white">Transport Maritime</a></li>
<li><a href="#" className="hover:text-white">Logistique</a></li>
<li><a href="#" className="hover:text-white">Douanes</a></li>
<li><a href="#" className="hover:text-white">Assurance</a></li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Entreprise</h3>
<ul className="space-y-2 text-gray-400">
<li><a href="#" className="hover:text-white">À propos</a></li>
<li><a href="#" className="hover:text-white">Carrières</a></li>
<li><a href="#" className="hover:text-white">Actualités</a></li>
<li><a href="#" className="hover:text-white">Partenaires</a></li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Contact</h3>
<div className="space-y-3 text-gray-400">
<div className="flex items-center">
<Phone className="h-4 w-4 mr-2" />
<span>+33 1 23 45 67 89</span>
</div>
<div className="flex items-center">
<Mail className="h-4 w-4 mr-2" />
<span>contact@xpeditis.com</span>
</div>
<div className="flex items-center">
<LocationIcon className="h-4 w-4 mr-2" />
<span>Paris, France</span>
</div>
</div>
</div>
</div>
<div className="border-t border-gray-800 mt-12 pt-8 text-center text-gray-400">
<p>&copy; 2024 Xpeditis. Tous droits réservés.</p>
</div>
</div>
</footer>
</div>
);
};
export default LandingPage;

231
src/pages/LoginPage.tsx Normal file
View File

@ -0,0 +1,231 @@
import { useState, useEffect } from 'react';
import { Eye, EyeOff, Ship, Mail, Lock, ArrowRight } from 'lucide-react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();
const location = useLocation();
const { login, isAuthenticated } = useAuth();
// Rediriger si déjà connecté
useEffect(() => {
if (isAuthenticated) {
const from = (location.state as any)?.from?.pathname || '/app';
navigate(from, { replace: true });
}
}, [isAuthenticated, navigate, location]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const success = await login(email, password);
if (success) {
const from = (location.state as any)?.from?.pathname || '/app';
navigate(from, { replace: true });
} else {
setError('Email ou mot de passe incorrect');
}
} catch (error) {
setError('Une erreur est survenue lors de la connexion');
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<div className="max-w-md w-full space-y-8">
{/* Logo et titre */}
<div className="text-center">
<div className="flex items-center justify-center mb-6">
<div className="bg-blue-600 p-3 rounded-xl">
<Ship className="h-8 w-8 text-white" />
</div>
</div>
<h2 className="text-3xl font-bold text-gray-900">
Connexion à Xpeditis
</h2>
<p className="mt-2 text-gray-600">
Accédez à votre plateforme maritime
</p>
</div>
{/* Formulaire de connexion */}
<div className="maritime-card p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Adresse email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="votre@email.com"
/>
</div>
</div>
{/* Mot de passe */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Mot de passe
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
{/* Options */}
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
Se souvenir de moi
</label>
</div>
<Link
to="/forgot-password"
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
>
Mot de passe oublié ?
</Link>
</div>
{/* Message d'erreur */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
{/* Bouton de connexion */}
<button
type="submit"
disabled={isLoading}
className="w-full maritime-button py-3 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
Connexion en cours...
</div>
) : (
<div className="flex items-center justify-center">
Se connecter
<ArrowRight className="ml-2 h-4 w-4" />
</div>
)}
</button>
</form>
{/* Informations de démonstration */}
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<h4 className="text-sm font-medium text-blue-800 mb-2">
Compte de démonstration
</h4>
<div className="text-sm text-blue-700 space-y-1">
<p><strong>Email :</strong> demo@xpeditis.com</p>
<p><strong>Mot de passe :</strong> demo123</p>
</div>
</div>
{/* Lien d'inscription */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Pas encore de compte ?{' '}
<Link
to="/register"
className="text-blue-600 hover:text-blue-700 font-medium"
>
Créer un compte
</Link>
</p>
</div>
</div>
{/* Fonctionnalités */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
<div className="p-4">
<div className="bg-blue-100 w-12 h-12 rounded-lg flex items-center justify-center mx-auto mb-2">
<Ship className="h-6 w-6 text-blue-600" />
</div>
<h3 className="text-sm font-medium text-gray-900">Suivi en temps réel</h3>
<p className="text-xs text-gray-600 mt-1">
Suivez vos expéditions 24h/24
</p>
</div>
<div className="p-4">
<div className="bg-green-100 w-12 h-12 rounded-lg flex items-center justify-center mx-auto mb-2">
<Mail className="h-6 w-6 text-green-600" />
</div>
<h3 className="text-sm font-medium text-gray-900">Devis instantané</h3>
<p className="text-xs text-gray-600 mt-1">
Obtenez vos prix en quelques clics
</p>
</div>
<div className="p-4">
<div className="bg-purple-100 w-12 h-12 rounded-lg flex items-center justify-center mx-auto mb-2">
<Lock className="h-6 w-6 text-purple-600" />
</div>
<h3 className="text-sm font-medium text-gray-900">Sécurisé</h3>
<p className="text-xs text-gray-600 mt-1">
Vos données sont protégées
</p>
</div>
</div>
{/* Footer */}
<div className="text-center text-xs text-gray-500">
<p>
© 2025 Xpeditis. Tous droits réservés.{' '}
<Link to="/privacy" className="hover:text-gray-700">
Politique de confidentialité
</Link>
{' • '}
<Link to="/terms" className="hover:text-gray-700">
Conditions d'utilisation
</Link>
</p>
</div>
</div>
</div>
);
};
export default LoginPage;

320
src/pages/QuotePage.tsx Normal file
View File

@ -0,0 +1,320 @@
import { useState } from 'react';
import { ArrowRight, MapPin, Package, Truck, Ship, CheckCircle, DollarSign, Calendar } from 'lucide-react';
interface QuoteForm {
operation: 'import' | 'export' | '';
incoterm: 'dock-to-door' | 'door-to-dock' | 'door-to-door' | '';
originPort: string;
originWarehouse: string;
destinationPort: string;
destinationWarehouse: string;
cargoType: string;
departureDate: string;
weight: string;
volume: string;
}
interface Quote {
id: string;
serviceType: string;
price: number;
transitTime: string;
carrier: string;
features: string[];
}
const QuotePage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<QuoteForm>({
operation: '',
incoterm: '',
originPort: '',
originWarehouse: '',
destinationPort: '',
destinationWarehouse: '',
cargoType: '',
departureDate: '',
weight: '',
volume: ''
});
const [isLoading, setIsLoading] = useState(false);
const [quotes, setQuotes] = useState<Quote[]>([]);
const steps = [
{ id: 1, name: 'General Info', completed: false },
{ id: 2, name: 'Conditionnement', completed: false },
{ id: 3, name: 'téléchargement', completed: false }
];
const handleNext = () => {
if (currentStep < 3) {
setCurrentStep(currentStep + 1);
}
};
const handleOperationSelect = (operation: 'import' | 'export') => {
setFormData({ ...formData, operation });
};
const handleIncotermSelect = (incoterm: 'dock-to-door' | 'door-to-dock' | 'door-to-door') => {
setFormData({ ...formData, incoterm });
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// Simulation d'appel API
setTimeout(() => {
const mockQuotes = [
{
id: 'quote-express-001',
serviceType: 'Express',
price: 2850,
transitTime: '12-15 jours',
carrier: 'Maritime Express Line',
features: ['Priorité d\'embarquement', 'Suivi premium', 'Assurance incluse'],
},
{
id: 'quote-standard-001',
serviceType: 'Standard',
price: 1950,
transitTime: '18-22 jours',
carrier: 'Global Shipping Co.',
features: ['Suivi standard', 'Service client 24/7', 'Documentation complète'],
},
{
id: 'quote-economy-001',
serviceType: 'Économique',
price: 1450,
transitTime: '25-30 jours',
carrier: 'Budget Maritime',
features: ['Prix compétitif', 'Suivi de base', 'Flexibilité des dates'],
},
];
setQuotes(mockQuotes);
setIsLoading(false);
}, 2000);
};
const ports = [
'Le Havre, France',
'Marseille, France',
'Rotterdam, Pays-Bas',
'Hamburg, Allemagne',
'Shanghai, Chine',
'New York, États-Unis',
'Los Angeles, États-Unis',
'Singapore',
'Dubai, EAU',
'Hong Kong',
];
return (
<div className="max-w-4xl mx-auto space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Demande de devis maritime
</h1>
<p className="text-lg text-gray-600">
Obtenez instantanément les meilleurs tarifs pour vos expéditions
</p>
</div>
{/* Formulaire de devis */}
<div className="maritime-card p-8">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Port d'origine */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<MapPin className="inline h-4 w-4 mr-1" />
Port d'origine
</label>
<select
name="originPort"
value={formData.originPort}
onChange={handleInputChange}
required
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Sélectionner un port</option>
{ports.map(port => (
<option key={port} value={port}>{port}</option>
))}
</select>
</div>
{/* Port de destination */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<MapPin className="inline h-4 w-4 mr-1" />
Port de destination
</label>
<select
name="destinationPort"
value={formData.destinationPort}
onChange={handleInputChange}
required
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Sélectionner un port</option>
{ports.map(port => (
<option key={port} value={port}>{port}</option>
))}
</select>
</div>
{/* Type de marchandise */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Package className="inline h-4 w-4 mr-1" />
Type de marchandise
</label>
<select
name="cargoType"
value={formData.cargoType}
onChange={handleInputChange}
required
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Sélectionner le type</option>
<option value="general">Marchandise générale</option>
<option value="container">Conteneur FCL</option>
<option value="lcl">Groupage LCL</option>
<option value="dangerous">Matières dangereuses</option>
<option value="refrigerated">Produits réfrigérés</option>
</select>
</div>
{/* Date de départ */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Calendar className="inline h-4 w-4 mr-1" />
Date de départ souhaitée
</label>
<input
type="date"
name="departureDate"
value={formData.departureDate}
onChange={handleInputChange}
required
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Poids */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Poids (kg)
</label>
<input
type="number"
name="weight"
value={formData.weight}
onChange={handleInputChange}
required
placeholder="Ex: 1500"
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Volume */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Volume (m³)
</label>
<input
type="number"
name="volume"
value={formData.volume}
onChange={handleInputChange}
required
placeholder="Ex: 2.5"
step="0.1"
className="w-full maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full maritime-button py-4 text-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
Recherche en cours...
</div>
) : (
<>
<DollarSign className="inline h-5 w-5 mr-2" />
Obtenir mes devis
</>
)}
</button>
</form>
</div>
{/* Résultats des devis */}
{quotes.length > 0 && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-900">
Vos devis personnalisés
</h2>
<div className="grid gap-6">
{quotes.map((quote) => (
<div key={quote.id} className="maritime-card p-6 hover:shadow-lg transition-shadow">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-semibold text-gray-900 mb-1">
Service {quote.serviceType}
</h3>
<p className="text-gray-600">{quote.carrier}</p>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-blue-600">
{quote.price.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
Transit: {quote.transitTime}
</div>
</div>
</div>
<div className="mb-4">
<h4 className="font-medium text-gray-900 mb-2">Services inclus:</h4>
<ul className="space-y-1">
{quote.features.map((feature, index) => (
<li key={index} className="text-sm text-gray-600 flex items-center">
<div className="w-2 h-2 bg-blue-600 rounded-full mr-2"></div>
{feature}
</li>
))}
</ul>
</div>
<div className="flex gap-3">
<button className="maritime-button flex-1">
Réserver maintenant
</button>
<button className="border border-gray-300 text-gray-700 hover:bg-gray-50 px-6 py-2 rounded-lg font-medium transition-colors">
Détails
</button>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default QuotePage;

View File

@ -0,0 +1,326 @@
import { useState } from 'react';
import { Ship, Calendar, MapPin, Clock, Filter, Search } from 'lucide-react';
interface Schedule {
id: string;
vesselName: string;
route: string;
departure: {
port: string;
date: string;
time: string;
};
arrival: {
port: string;
date: string;
time: string;
};
transitTime: string;
capacity: {
available: number;
total: number;
};
price: number;
service: 'Express' | 'Standard' | 'Économique';
}
const ShipSchedulePage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedService, setSelectedService] = useState<string>('all');
const schedules: Schedule[] = [
{
id: '1',
vesselName: 'MSC Bellissima',
route: 'Europe - Asie',
departure: {
port: 'Le Havre, France',
date: '2025-01-15',
time: '08:00',
},
arrival: {
port: 'Shanghai, Chine',
date: '2025-02-05',
time: '14:00',
},
transitTime: '21 jours',
capacity: {
available: 45,
total: 120,
},
price: 1950,
service: 'Standard',
},
{
id: '2',
vesselName: 'CMA CGM Antoine de Saint Exupéry',
route: 'Europe - Amérique du Nord',
departure: {
port: 'Marseille, France',
date: '2025-01-20',
time: '12:00',
},
arrival: {
port: 'New York, États-Unis',
date: '2025-02-12',
time: '09:00',
},
transitTime: '23 jours',
capacity: {
available: 28,
total: 80,
},
price: 2850,
service: 'Express',
},
{
id: '3',
vesselName: 'Hapag-Lloyd Berlin Express',
route: 'Europe - Europe',
departure: {
port: 'Rotterdam, Pays-Bas',
date: '2025-01-18',
time: '16:00',
},
arrival: {
port: 'Hamburg, Allemagne',
date: '2025-01-20',
time: '10:00',
},
transitTime: '2 jours',
capacity: {
available: 67,
total: 100,
},
price: 1450,
service: 'Économique',
},
{
id: '4',
vesselName: 'COSCO Shipping Universe',
route: 'Asie - Europe',
departure: {
port: 'Singapore',
date: '2025-01-25',
time: '20:00',
},
arrival: {
port: 'Rotterdam, Pays-Bas',
date: '2025-02-18',
time: '06:00',
},
transitTime: '24 jours',
capacity: {
available: 89,
total: 150,
},
price: 2200,
service: 'Standard',
},
];
const filteredSchedules = schedules.filter(schedule => {
const matchesSearch = schedule.vesselName.toLowerCase().includes(searchTerm.toLowerCase()) ||
schedule.route.toLowerCase().includes(searchTerm.toLowerCase()) ||
schedule.departure.port.toLowerCase().includes(searchTerm.toLowerCase()) ||
schedule.arrival.port.toLowerCase().includes(searchTerm.toLowerCase());
const matchesService = selectedService === 'all' || schedule.service === selectedService;
return matchesSearch && matchesService;
});
const getServiceColor = (service: string) => {
switch (service) {
case 'Express':
return 'bg-red-100 text-red-800';
case 'Standard':
return 'bg-blue-100 text-blue-800';
case 'Économique':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getCapacityColor = (available: number, total: number) => {
const percentage = (available / total) * 100;
if (percentage > 60) return 'text-green-600';
if (percentage > 30) return 'text-yellow-600';
return 'text-red-600';
};
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">Planning des navires</h1>
<p className="text-gray-600 mt-2">
Consultez les horaires et disponibilités des navires
</p>
</div>
{/* Filtres et recherche */}
<div className="maritime-card p-6">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Rechercher par navire, route ou port..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-4">
<select
value={selectedService}
onChange={(e) => setSelectedService(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tous les services</option>
<option value="Express">Express</option>
<option value="Standard">Standard</option>
<option value="Économique">Économique</option>
</select>
<button className="flex items-center px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<Filter className="h-4 w-4 mr-2" />
Plus de filtres
</button>
</div>
</div>
</div>
{/* Statistiques rapides */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-blue-600 mb-2">{schedules.length}</div>
<div className="text-sm text-gray-600">Navires programmés</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-green-600 mb-2">
{schedules.reduce((acc, s) => acc + s.capacity.available, 0)}
</div>
<div className="text-sm text-gray-600">Places disponibles</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-yellow-600 mb-2">12</div>
<div className="text-sm text-gray-600">Routes actives</div>
</div>
<div className="maritime-card p-6 text-center">
<div className="text-2xl font-bold text-gray-900 mb-2">18j</div>
<div className="text-sm text-gray-600">Transit moyen</div>
</div>
</div>
{/* Liste des horaires */}
<div className="space-y-4">
{filteredSchedules.map((schedule) => (
<div key={schedule.id} className="maritime-card p-6 hover:shadow-lg transition-shadow">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
{/* Informations du navire */}
<div className="flex-1">
<div className="flex items-center space-x-4 mb-3">
<Ship className="h-6 w-6 text-blue-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
{schedule.vesselName}
</h3>
<p className="text-sm text-gray-600">{schedule.route}</p>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getServiceColor(schedule.service)}`}>
{schedule.service}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Départ */}
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-2 h-2 bg-green-500 rounded-full mt-2"></div>
<div>
<div className="text-sm text-gray-600">Départ</div>
<div className="font-medium">{schedule.departure.port}</div>
<div className="text-sm text-gray-600 flex items-center mt-1">
<Calendar className="h-4 w-4 mr-1" />
{new Date(schedule.departure.date).toLocaleDateString('fr-FR')} à {schedule.departure.time}
</div>
</div>
</div>
{/* Arrivée */}
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-2 h-2 bg-red-500 rounded-full mt-2"></div>
<div>
<div className="text-sm text-gray-600">Arrivée</div>
<div className="font-medium">{schedule.arrival.port}</div>
<div className="text-sm text-gray-600 flex items-center mt-1">
<Calendar className="h-4 w-4 mr-1" />
{new Date(schedule.arrival.date).toLocaleDateString('fr-FR')} à {schedule.arrival.time}
</div>
</div>
</div>
</div>
</div>
{/* Informations de capacité et prix */}
<div className="flex flex-col lg:flex-row items-start lg:items-center space-y-4 lg:space-y-0 lg:space-x-8">
<div className="text-center">
<div className="text-sm text-gray-600">Durée de transit</div>
<div className="font-semibold text-gray-900 flex items-center">
<Clock className="h-4 w-4 mr-1" />
{schedule.transitTime}
</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-600">Disponibilité</div>
<div className={`font-semibold ${getCapacityColor(schedule.capacity.available, schedule.capacity.total)}`}>
{schedule.capacity.available}/{schedule.capacity.total}
</div>
<div className="text-xs text-gray-500">conteneurs</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-600">À partir de</div>
<div className="text-2xl font-bold text-gray-900">
{schedule.price.toLocaleString()}
</div>
</div>
<button className="maritime-button whitespace-nowrap">
Réserver
</button>
</div>
</div>
</div>
))}
</div>
{/* Message si aucun résultat */}
{filteredSchedules.length === 0 && (
<div className="text-center py-12">
<Ship className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Aucun navire trouvé
</h3>
<p className="text-gray-600 mb-6">
Aucun navire ne correspond à vos critères de recherche.
</p>
<button
onClick={() => {
setSearchTerm('');
setSelectedService('all');
}}
className="maritime-button"
>
Réinitialiser les filtres
</button>
</div>
)}
</div>
);
};
export default ShipSchedulePage;

294
src/pages/TrackingPage.tsx Normal file
View File

@ -0,0 +1,294 @@
import { useState } from 'react';
import { Search, MapPin, Ship, Package, Clock, CheckCircle, AlertCircle } from 'lucide-react';
interface TrackingEvent {
id: string;
date: string;
time: string;
location: string;
description: string;
status: 'completed' | 'current' | 'pending';
}
interface TrackingInfo {
reference: string;
origin: string;
destination: string;
currentLocation: string;
estimatedArrival: string;
progress: number;
events: TrackingEvent[];
}
const TrackingPage: React.FC = () => {
const [trackingNumber, setTrackingNumber] = useState('');
const [trackingInfo, setTrackingInfo] = useState<TrackingInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!trackingNumber.trim()) return;
setIsLoading(true);
setError('');
// Simulation d'appel API
setTimeout(() => {
if (trackingNumber === 'XPD-2025-001') {
setTrackingInfo({
reference: 'XPD-2025-001',
origin: 'Le Havre, France',
destination: 'Shanghai, Chine',
currentLocation: 'En mer - Océan Indien',
estimatedArrival: '2025-02-05',
progress: 65,
events: [
{
id: '1',
date: '2025-01-15',
time: '08:30',
location: 'Le Havre, France',
description: 'Conteneur chargé à bord du navire MSC Bellissima',
status: 'completed',
},
{
id: '2',
date: '2025-01-16',
time: '14:00',
location: 'Manche',
description: 'Navire en route vers la Méditerranée',
status: 'completed',
},
{
id: '3',
date: '2025-01-20',
time: '09:15',
location: 'Canal de Suez, Égypte',
description: 'Passage du canal de Suez',
status: 'completed',
},
{
id: '4',
date: '2025-01-25',
time: '16:45',
location: 'Océan Indien',
description: 'Navigation en cours vers l\'Asie',
status: 'current',
},
{
id: '5',
date: '2025-02-02',
time: '--:--',
location: 'Détroit de Malacca',
description: 'Passage prévu du détroit de Malacca',
status: 'pending',
},
{
id: '6',
date: '2025-02-05',
time: '--:--',
location: 'Shanghai, Chine',
description: 'Arrivée prévue au port de Shanghai',
status: 'pending',
},
],
});
} else {
setError('Numéro de suivi non trouvé. Vérifiez votre référence.');
setTrackingInfo(null);
}
setIsLoading(false);
}, 1500);
};
const getEventIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircle className="h-5 w-5 text-green-600" />;
case 'current':
return <AlertCircle className="h-5 w-5 text-blue-600" />;
case 'pending':
return <Clock className="h-5 w-5 text-gray-400" />;
default:
return <Clock className="h-5 w-5 text-gray-400" />;
}
};
return (
<div className="max-w-4xl mx-auto space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Suivi d'expédition
</h1>
<p className="text-lg text-gray-600">
Suivez votre expédition en temps réel
</p>
</div>
{/* Formulaire de recherche */}
<div className="maritime-card p-8">
<form onSubmit={handleSearch} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Numéro de suivi
</label>
<div className="flex gap-4">
<input
type="text"
value={trackingNumber}
onChange={(e) => setTrackingNumber(e.target.value)}
placeholder="Ex: XPD-2025-001"
className="flex-1 maritime-input p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading}
className="maritime-button px-8 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Recherche...
</div>
) : (
<>
<Search className="h-4 w-4 mr-2" />
Suivre
</>
)}
</button>
</div>
</div>
{error && (
<div className="text-red-600 text-sm mt-2">{error}</div>
)}
</form>
{/* Exemple de numéros */}
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800 mb-2">
<strong>Numéro d'exemple pour test :</strong>
</p>
<button
onClick={() => setTrackingNumber('XPD-2025-001')}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
XPD-2025-001
</button>
</div>
</div>
{/* Informations de suivi */}
{trackingInfo && (
<div className="space-y-6">
{/* Résumé de l'expédition */}
<div className="maritime-card p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900">
Expédition {trackingInfo.reference}
</h2>
<div className="text-right">
<div className="text-sm text-gray-600">Progression</div>
<div className="text-2xl font-bold text-blue-600">
{trackingInfo.progress}%
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="flex items-center">
<MapPin className="h-5 w-5 text-blue-600 mr-3" />
<div>
<div className="text-sm text-gray-600">Origine</div>
<div className="font-medium">{trackingInfo.origin}</div>
</div>
</div>
<div className="flex items-center">
<Ship className="h-5 w-5 text-blue-600 mr-3" />
<div>
<div className="text-sm text-gray-600">Position actuelle</div>
<div className="font-medium">{trackingInfo.currentLocation}</div>
</div>
</div>
<div className="flex items-center">
<Package className="h-5 w-5 text-blue-600 mr-3" />
<div>
<div className="text-sm text-gray-600">Destination</div>
<div className="font-medium">{trackingInfo.destination}</div>
</div>
</div>
</div>
{/* Barre de progression */}
<div className="mb-4">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Progression du voyage</span>
<span>Arrivée prévue: {new Date(trackingInfo.estimatedArrival).toLocaleDateString('fr-FR')}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-500"
style={{ width: `${trackingInfo.progress}%` }}
></div>
</div>
</div>
</div>
{/* Historique des événements */}
<div className="maritime-card p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-6">
Historique de l'expédition
</h3>
<div className="space-y-4">
{trackingInfo.events.map((event, index) => (
<div key={event.id} className="flex items-start space-x-4">
<div className="flex-shrink-0 mt-1">
{getEventIcon(event.status)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className={`text-sm font-medium ${
event.status === 'completed' ? 'text-gray-900' :
event.status === 'current' ? 'text-blue-600' : 'text-gray-500'
}`}>
{event.description}
</p>
<div className="text-right text-sm text-gray-500">
<div>{new Date(event.date).toLocaleDateString('fr-FR')}</div>
{event.time !== '--:--' && <div>{event.time}</div>}
</div>
</div>
<p className="text-sm text-gray-600 mt-1">
{event.location}
</p>
</div>
</div>
))}
</div>
</div>
{/* Actions */}
<div className="maritime-card p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Actions disponibles
</h3>
<div className="flex flex-wrap gap-3">
<button className="maritime-button">
Recevoir des notifications
</button>
<button className="border border-gray-300 text-gray-700 hover:bg-gray-50 px-4 py-2 rounded-lg font-medium transition-colors">
Télécharger le rapport
</button>
<button className="border border-gray-300 text-gray-700 hover:bg-gray-50 px-4 py-2 rounded-lg font-medium transition-colors">
Partager le suivi
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default TrackingPage;

78
tailwind.config.js Normal file
View File

@ -0,0 +1,78 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'./index.html',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
}

35
vite.config.ts Normal file
View File

@ -0,0 +1,35 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false,
configure: (proxy, _options) => {
proxy.on('error', (err, _req, _res) => {
console.log('proxy error', err);
});
proxy.on('proxyReq', (proxyReq, req, _res) => {
console.log('Sending Request to the Target:', req.method, req.url);
});
proxy.on('proxyRes', (proxyRes, req, _res) => {
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
});
},
}
}
},
build: {
outDir: 'dist',
sourcemap: true,
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
}
})