From 7f31aa59a9b97d6dc3006cfa43eca58a32b0f248 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 17 Mar 2026 11:58:29 +0100 Subject: [PATCH 01/23] fix contact --- CLAUDE.md | 41 +++++--- apps/frontend/app/contact/page.tsx | 161 +++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d210ccc..2ae51dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,15 +91,24 @@ Docker-compose defaults (no `.env` changes needed for local dev): ``` apps/backend/src/ ├── domain/ # CORE - Pure TypeScript, NO framework imports -│ ├── entities/ # Booking, RateQuote, User, Carrier, Port, Container, CsvBooking, etc. -│ ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, LicenseStatus, SubscriptionPlan, etc. -│ ├── services/ # Pure domain services (rate-search, csv-rate-price-calculator, booking, port-search, etc.) +│ ├── entities/ # Booking, RateQuote, Carrier, Port, Container, Notification, Webhook, AuditLog +│ ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, etc. +│ ├── services/ # Pure domain services (csv-rate-price-calculator) │ ├── ports/ │ │ ├── in/ # Use case interfaces with execute() method │ │ └── out/ # Repository/SPI interfaces (token constants like BOOKING_REPOSITORY = 'BookingRepository') │ └── exceptions/ # Domain-specific exceptions ├── application/ # Controllers, DTOs (class-validator), Guards, Decorators, Mappers -└── infrastructure/ # TypeORM entities/repos/mappers, Redis cache, carrier APIs, MinIO/S3, email (MJML+Nodemailer), Sentry +│ ├── [feature]/ # Feature modules grouped by domain (auth/, bookings/, rates/, etc.) +│ ├── controllers/ # REST controllers (also nested under feature folders) +│ ├── services/ # Application services (audit, notification, webhook, booking-automation, export, etc.) +│ ├── gateways/ # WebSocket gateways (notifications.gateway.ts via Socket.IO) +│ ├── guards/ # JwtAuthGuard, RolesGuard, CustomThrottlerGuard +│ ├── decorators/ # @Public(), @Roles(), @CurrentUser() +│ ├── dto/ # Request/response DTOs with class-validator +│ ├── mappers/ # Domain ↔ DTO mappers +│ └── interceptors/ # PerformanceMonitoringInterceptor +└── infrastructure/ # TypeORM entities/repos/mappers, Redis cache, carrier APIs, MinIO/S3, email (MJML+Nodemailer), Stripe, Sentry ``` **Critical dependency rules**: @@ -108,6 +117,7 @@ apps/backend/src/ - Path aliases: `@domain/*`, `@application/*`, `@infrastructure/*` (defined in `apps/backend/tsconfig.json`) - Domain tests run without NestJS TestingModule - Backend has strict TypeScript: `strict: true`, `strictNullChecks: true` (but `strictPropertyInitialization: false`) +- Env vars validated at startup via Joi schema in `app.module.ts` — required vars include DATABASE_*, REDIS_*, JWT_SECRET, SMTP_* ### NestJS Modules (app.module.ts) @@ -115,31 +125,36 @@ Global guards: JwtAuthGuard (all routes protected by default), CustomThrottlerGu Feature modules: Auth, Rates, Ports, Bookings, CsvBookings, Organizations, Users, Dashboard, Audit, Notifications, Webhooks, GDPR, Admin, Subscriptions. -Infrastructure modules: CacheModule, CarrierModule, SecurityModule, CsvRateModule. +Infrastructure modules: CacheModule, CarrierModule, SecurityModule, CsvRateModule, StripeModule, PdfModule, StorageModule, EmailModule. -Swagger plugin enabled in `nest-cli.json` — DTOs auto-documented. +Swagger plugin enabled in `nest-cli.json` — DTOs auto-documented. Logging via `nestjs-pino` (pino-pretty in dev). ### Frontend (Next.js 14 App Router) ``` apps/frontend/ -├── app/ # App Router pages -│ ├── dashboard/ # Protected routes (bookings, admin, settings) -│ └── carrier/ # Carrier portal (magic link auth) +├── app/ # App Router pages (root-level) +│ ├── dashboard/ # Protected routes (bookings, admin, settings, wiki, search) +│ ├── carrier/ # Carrier portal (magic link auth — accept/reject/documents) +│ ├── booking/ # Booking confirmation/rejection flows +│ └── [auth pages] # login, register, forgot-password, verify-email └── src/ - ├── components/ # React components (shadcn/ui in ui/, layout/, bookings/, admin/) + ├── app/ # Additional app pages (e.g. rates/csv-search) + ├── components/ # React components (ui/, layout/, bookings/, admin/, rate-search/, organization/) ├── hooks/ # useBookings, useNotifications, useCsvRateSearch, useCompanies, useFilterOptions ├── lib/ │ ├── api/ # Fetch-based API client with auto token refresh (client.ts + per-module files) - │ ├── context/ # Auth context + │ ├── context/ # Auth context, cookie context + │ ├── providers/ # QueryProvider (TanStack Query / React Query) │ └── fonts.ts # Manrope (headings) + Montserrat (body) ├── types/ # TypeScript type definitions - └── utils/ # Export utilities (Excel, PDF) + ├── utils/ # Export utilities (Excel, PDF) + └── legacy-pages/ # Archived page components (BookingsManagement, CarrierManagement, CarrierMonitoring) ``` Path aliases: `@/*` → `./src/*`, `@/components/*`, `@/lib/*`, `@/app/*` → `./app/*`, `@/types/*`, `@/hooks/*`, `@/utils/*` -**Note**: Frontend tsconfig has `strict: false`, `noImplicitAny: false`, `strictNullChecks: false` (unlike backend which is strict). +**Note**: Frontend tsconfig has `strict: false`, `noImplicitAny: false`, `strictNullChecks: false` (unlike backend which is strict). Uses TanStack Query (React Query) for server state — wrap new data fetching in hooks, not bare `fetch` calls. ### Brand Design diff --git a/apps/frontend/app/contact/page.tsx b/apps/frontend/app/contact/page.tsx index e741ff0..9523ffe 100644 --- a/apps/frontend/app/contact/page.tsx +++ b/apps/frontend/app/contact/page.tsx @@ -13,6 +13,10 @@ import { Building2, CheckCircle2, Loader2, + Shield, + Zap, + BookOpen, + ArrowRight, } from 'lucide-react'; import { LandingHeader, LandingFooter } from '@/components/layout'; @@ -33,10 +37,14 @@ export default function ContactPage() { const heroRef = useRef(null); const formRef = useRef(null); const contactRef = useRef(null); + const afterSubmitRef = useRef(null); + const quickAccessRef = useRef(null); const isHeroInView = useInView(heroRef, { once: true }); const isFormInView = useInView(formRef, { once: true }); const isContactInView = useInView(contactRef, { once: true }); + const isAfterSubmitInView = useInView(afterSubmitRef, { once: true }); + const isQuickAccessInView = useInView(quickAccessRef, { once: true }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -526,6 +534,159 @@ export default function ContactPage() { + {/* Section 1 : Ce qui se passe après l'envoi */} +
+
+ + {/* Decorative blobs */} +
+
+
+
+ +
+
+
+ +
+ + Après votre envoi + +
+

+ Que se passe-t-il après l'envoi de votre message ? +

+ +
+ {/* Notre engagement */} + +
+
+ +
+

Notre engagement

+
+

+ Dès réception de votre demande, un de nos experts logistiques analyse votre + profil et vos besoins. Vous recevrez une réponse personnalisée ou une invitation + pour une démonstration de la plateforme{' '} + + sous 48 heures ouvrées. + +

+
+ + {/* Sécurité */} + +
+
+ +
+

Sécurité

+
+

+ Vos informations sont protégées et traitées conformément à notre{' '} + + politique de confidentialité + + . Aucune donnée n'est partagée avec des tiers sans votre accord. +

+
+
+
+ +
+
+ + {/* Section 2 : Accès Rapide */} +
+
+ +
+ + Accès rapide + +

+ Besoin d'une réponse immédiate ? +

+
+ +
+ {/* Tarification instantanée */} + +
+ +
+

Tarification instantanée

+

+ N'attendez pas notre retour pour vos prix. Utilisez notre moteur{' '} + Click&Ship pour obtenir + une cotation de fret maritime en moins de 60 secondes. +

+ + Accéder au Dashboard + + +
+ + {/* Wiki Maritime */} + +
+ +
+

Aide rapide

+

+ Une question sur les Incoterms ou la documentation export ? Notre{' '} + Wiki Maritime contient déjà + les réponses aux questions les plus fréquentes. +

+ + Consulter le Wiki + + +
+
+
+
+
+ {/* Map Section */}
From 1c6edb9d41072bea15e61b2ed4bd6354375ab2e8 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 17 Mar 2026 18:39:07 +0100 Subject: [PATCH 02/23] adding middleware for auth --- apps/frontend/app/about/page.tsx | 36 ++- apps/frontend/app/dashboard/layout.tsx | 25 +- apps/frontend/app/login/page.tsx | 17 +- apps/frontend/app/page.tsx | 288 +++++++++++++----- apps/frontend/middleware.ts | 31 +- .../src/components/layout/LandingHeader.tsx | 20 +- apps/frontend/src/lib/api/client.ts | 7 +- .../frontend/src/lib/context/auth-context.tsx | 8 +- 8 files changed, 315 insertions(+), 117 deletions(-) diff --git a/apps/frontend/app/about/page.tsx b/apps/frontend/app/about/page.tsx index caea7ca..b89b7c0 100644 --- a/apps/frontend/app/about/page.tsx +++ b/apps/frontend/app/about/page.tsx @@ -350,21 +350,30 @@ export default function AboutPage() {
- {/* Timeline line */} -
+ {/* Timeline vertical rail + animated fill */} +
+ +
{timeline.map((item, index) => (
-
-
+
+
{item.year}
@@ -372,9 +381,18 @@ export default function AboutPage() {

{item.description}

-
-
+ + {/* Animated center dot */} +
+
+
))} diff --git a/apps/frontend/app/dashboard/layout.tsx b/apps/frontend/app/dashboard/layout.tsx index b57a3c3..5bac1c5 100644 --- a/apps/frontend/app/dashboard/layout.tsx +++ b/apps/frontend/app/dashboard/layout.tsx @@ -8,8 +8,8 @@ import { useAuth } from '@/lib/context/auth-context'; import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; import NotificationDropdown from '@/components/NotificationDropdown'; import AdminPanelDropdown from '@/components/admin/AdminPanelDropdown'; import Image from 'next/image'; @@ -25,10 +25,29 @@ import { } from 'lucide-react'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { - const { user, logout } = useAuth(); + const { user, logout, loading, isAuthenticated } = useAuth(); const pathname = usePathname(); + const router = useRouter(); const [sidebarOpen, setSidebarOpen] = useState(false); + useEffect(() => { + if (!loading && !isAuthenticated) { + router.replace(`/login?redirect=${encodeURIComponent(pathname)}`); + } + }, [loading, isAuthenticated, router, pathname]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return null; + } + const navigation = [ { name: 'Tableau de bord', href: '/dashboard', icon: BarChart3 }, { name: 'Réservations', href: '/dashboard/bookings', icon: Package }, diff --git a/apps/frontend/app/login/page.tsx b/apps/frontend/app/login/page.tsx index 17ed57d..d06a37d 100644 --- a/apps/frontend/app/login/page.tsx +++ b/apps/frontend/app/login/page.tsx @@ -8,9 +8,10 @@ 'use client'; -import { useState } from 'react'; +import { useState, Suspense } from 'react'; import Link from 'next/link'; import Image from 'next/image'; +import { useSearchParams } from 'next/navigation'; import { useAuth } from '@/lib/context/auth-context'; interface FieldErrors { @@ -73,8 +74,10 @@ function getErrorMessage(error: any): { message: string; field?: 'email' | 'pass }; } -export default function LoginPage() { +function LoginPageContent() { const { login } = useAuth(); + const searchParams = useSearchParams(); + const redirectTo = searchParams.get('redirect') || '/dashboard'; const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [rememberMe, setRememberMe] = useState(false); @@ -126,7 +129,7 @@ export default function LoginPage() { setIsLoading(true); try { - await login(email, password); + await login(email, password, redirectTo); // Navigation is handled by the login function in auth context } catch (err: any) { const { message, field } = getErrorMessage(err); @@ -462,3 +465,11 @@ export default function LoginPage() {
); } + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/page.tsx index 7af29bf..be04465 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRef } from 'react'; +import { useRef, useState, useEffect } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { motion, useInView, useScroll, useTransform } from 'framer-motion'; @@ -27,6 +27,43 @@ import { import { useAuth } from '@/lib/context/auth-context'; import { LandingHeader, LandingFooter } from '@/components/layout'; +function AnimatedCounter({ + end, + suffix = '', + prefix = '', + decimals = 0, + isActive, + duration = 2, +}: { + end: number; + suffix?: string; + prefix?: string; + decimals?: number; + isActive: boolean; + duration?: number; +}) { + const [count, setCount] = useState(0); + + useEffect(() => { + if (!isActive) return; + let startTime: number | undefined; + + const animate = (timestamp: number) => { + if (!startTime) startTime = timestamp; + const progress = Math.min((timestamp - startTime) / (duration * 1000), 1); + const eased = 1 - Math.pow(1 - progress, 3); + setCount(eased * end); + if (progress < 1) requestAnimationFrame(animate); + else setCount(end); + }; + + requestAnimationFrame(animate); + }, [end, duration, isActive]); + + const display = decimals > 0 ? count.toFixed(decimals) : Math.floor(count).toString(); + return <>{prefix}{display}{suffix}; +} + export default function LandingPage() { const { user, isAuthenticated } = useAuth(); @@ -37,14 +74,16 @@ export default function LandingPage() { const pricingRef = useRef(null); const testimonialsRef = useRef(null); const ctaRef = useRef(null); + const howRef = useRef(null); const isHeroInView = useInView(heroRef, { once: true }); const isFeaturesInView = useInView(featuresRef, { once: true }); - const isStatsInView = useInView(statsRef, { once: true }); + const isStatsInView = useInView(statsRef, { once: true, amount: 0.3 }); const isToolsInView = useInView(toolsRef, { once: true }); const isPricingInView = useInView(pricingRef, { once: true }); const isTestimonialsInView = useInView(testimonialsRef, { once: true }); const isCtaInView = useInView(ctaRef, { once: true }); + const isHowInView = useInView(howRef, { once: true, amount: 0.2 }); const { scrollYProgress } = useScroll(); const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '50%']); @@ -138,10 +177,10 @@ export default function LandingPage() { ]; const stats = [ - { value: '50+', label: 'Compagnies Maritimes', icon: Ship }, - { value: '10K+', label: 'Ports Mondiaux', icon: Anchor }, - { value: '<2s', label: 'Temps de Réponse', icon: Zap }, - { value: '99.5%', label: 'Disponibilité', icon: CheckCircle2 }, + { end: 50, prefix: '', suffix: '+', decimals: 0, label: 'Compagnies Maritimes', icon: Ship }, + { end: 10, prefix: '', suffix: 'K+', decimals: 0, label: 'Ports Mondiaux', icon: Anchor }, + { end: 2, prefix: '<', suffix: 's', decimals: 0, label: 'Temps de Réponse', icon: Zap }, + { end: 99.5, prefix: '', suffix: '%', decimals: 1, label: 'Disponibilité', icon: CheckCircle2 }, ]; const pricingPlans = [ @@ -252,20 +291,31 @@ export default function LandingPage() { {/* Hero Section */}
- {/* Background Image */} + {/* Background Video */} - {/* Container background image */} -
+