fix contact

This commit is contained in:
David 2026-03-17 11:58:29 +01:00
parent 737d94e99a
commit 7f31aa59a9
2 changed files with 189 additions and 13 deletions

View File

@ -91,15 +91,24 @@ Docker-compose defaults (no `.env` changes needed for local dev):
``` ```
apps/backend/src/ apps/backend/src/
├── domain/ # CORE - Pure TypeScript, NO framework imports ├── domain/ # CORE - Pure TypeScript, NO framework imports
│ ├── entities/ # Booking, RateQuote, User, Carrier, Port, Container, CsvBooking, etc. │ ├── entities/ # Booking, RateQuote, Carrier, Port, Container, Notification, Webhook, AuditLog
│ ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, LicenseStatus, SubscriptionPlan, etc. │ ├── value-objects/ # Money, Email, BookingNumber, BookingStatus, PortCode, ContainerType, Volume, etc.
│ ├── services/ # Pure domain services (rate-search, csv-rate-price-calculator, booking, port-search, etc.) │ ├── services/ # Pure domain services (csv-rate-price-calculator)
│ ├── ports/ │ ├── ports/
│ │ ├── in/ # Use case interfaces with execute() method │ │ ├── in/ # Use case interfaces with execute() method
│ │ └── out/ # Repository/SPI interfaces (token constants like BOOKING_REPOSITORY = 'BookingRepository') │ │ └── out/ # Repository/SPI interfaces (token constants like BOOKING_REPOSITORY = 'BookingRepository')
│ └── exceptions/ # Domain-specific exceptions │ └── exceptions/ # Domain-specific exceptions
├── application/ # Controllers, DTOs (class-validator), Guards, Decorators, Mappers ├── 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**: **Critical dependency rules**:
@ -108,6 +117,7 @@ apps/backend/src/
- Path aliases: `@domain/*`, `@application/*`, `@infrastructure/*` (defined in `apps/backend/tsconfig.json`) - Path aliases: `@domain/*`, `@application/*`, `@infrastructure/*` (defined in `apps/backend/tsconfig.json`)
- Domain tests run without NestJS TestingModule - Domain tests run without NestJS TestingModule
- Backend has strict TypeScript: `strict: true`, `strictNullChecks: true` (but `strictPropertyInitialization: false`) - 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) ### 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. 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) ### Frontend (Next.js 14 App Router)
``` ```
apps/frontend/ apps/frontend/
├── app/ # App Router pages ├── app/ # App Router pages (root-level)
│ ├── dashboard/ # Protected routes (bookings, admin, settings) │ ├── dashboard/ # Protected routes (bookings, admin, settings, wiki, search)
│ └── carrier/ # Carrier portal (magic link auth) │ ├── carrier/ # Carrier portal (magic link auth — accept/reject/documents)
│ ├── booking/ # Booking confirmation/rejection flows
│ └── [auth pages] # login, register, forgot-password, verify-email
└── src/ └── 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 ├── hooks/ # useBookings, useNotifications, useCsvRateSearch, useCompanies, useFilterOptions
├── lib/ ├── lib/
│ ├── api/ # Fetch-based API client with auto token refresh (client.ts + per-module files) │ ├── 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) │ └── fonts.ts # Manrope (headings) + Montserrat (body)
├── types/ # TypeScript type definitions ├── 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/*` 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 ### Brand Design

View File

@ -13,6 +13,10 @@ import {
Building2, Building2,
CheckCircle2, CheckCircle2,
Loader2, Loader2,
Shield,
Zap,
BookOpen,
ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import { LandingHeader, LandingFooter } from '@/components/layout'; import { LandingHeader, LandingFooter } from '@/components/layout';
@ -33,10 +37,14 @@ export default function ContactPage() {
const heroRef = useRef(null); const heroRef = useRef(null);
const formRef = useRef(null); const formRef = useRef(null);
const contactRef = useRef(null); const contactRef = useRef(null);
const afterSubmitRef = useRef(null);
const quickAccessRef = useRef(null);
const isHeroInView = useInView(heroRef, { once: true }); const isHeroInView = useInView(heroRef, { once: true });
const isFormInView = useInView(formRef, { once: true }); const isFormInView = useInView(formRef, { once: true });
const isContactInView = useInView(contactRef, { 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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -526,6 +534,159 @@ export default function ContactPage() {
</div> </div>
</section> </section>
{/* Section 1 : Ce qui se passe après l'envoi */}
<section ref={afterSubmitRef} className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="relative bg-gradient-to-br from-brand-navy to-brand-navy/90 rounded-3xl overflow-hidden p-8 lg:p-12"
>
{/* Decorative blobs */}
<div className="absolute inset-0 opacity-10 pointer-events-none">
<div className="absolute -top-10 -left-10 w-64 h-64 bg-brand-turquoise rounded-full blur-3xl" />
<div className="absolute -bottom-10 -right-10 w-64 h-64 bg-brand-green rounded-full blur-3xl" />
</div>
<div className="relative z-10">
<div className="flex items-center space-x-3 mb-2">
<div className="p-2 bg-brand-turquoise/20 rounded-lg">
<Mail className="w-5 h-5 text-brand-turquoise" />
</div>
<span className="text-brand-turquoise font-semibold uppercase tracking-widest text-xs">
Après votre envoi
</span>
</div>
<h2 className="text-2xl lg:text-3xl font-bold text-white mb-8">
Que se passe-t-il après l'envoi de votre message ?
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Notre engagement */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20"
>
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-brand-turquoise/30 rounded-xl flex items-center justify-center flex-shrink-0">
<CheckCircle2 className="w-5 h-5 text-brand-turquoise" />
</div>
<h3 className="text-lg font-bold text-white">Notre engagement</h3>
</div>
<p className="text-white/80 leading-relaxed">
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{' '}
<span className="text-brand-turquoise font-semibold">
sous 48 heures ouvrées.
</span>
</p>
</motion.div>
{/* Sécurité */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.35 }}
className="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20"
>
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-brand-green/30 rounded-xl flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-brand-green" />
</div>
<h3 className="text-lg font-bold text-white">Sécurité</h3>
</div>
<p className="text-white/80 leading-relaxed">
Vos informations sont protégées et traitées conformément à notre{' '}
<a href="/privacy" className="text-brand-turquoise font-semibold hover:underline">
politique de confidentialité
</a>
. Aucune donnée n'est partagée avec des tiers sans votre accord.
</p>
</motion.div>
</div>
</div>
</motion.div>
</div>
</section>
{/* Section 2 : Accès Rapide */}
<section ref={quickAccessRef} className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isQuickAccessInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7 }}
>
<div className="text-center mb-10">
<span className="text-brand-turquoise font-semibold uppercase tracking-widest text-xs">
Accès rapide
</span>
<h2 className="text-2xl lg:text-3xl font-bold text-brand-navy mt-2">
Besoin d'une réponse immédiate ?
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{/* Tarification instantanée */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={isQuickAccessInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.15 }}
whileHover={{ y: -4 }}
className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8 flex flex-col"
>
<div className="w-14 h-14 bg-gradient-to-br from-brand-turquoise to-cyan-400 rounded-2xl flex items-center justify-center mb-6 flex-shrink-0">
<Zap className="w-7 h-7 text-white" />
</div>
<h3 className="text-xl font-bold text-brand-navy mb-3">Tarification instantanée</h3>
<p className="text-gray-600 leading-relaxed flex-1 mb-6">
N'attendez pas notre retour pour vos prix. Utilisez notre moteur{' '}
<span className="font-semibold text-brand-navy">Click&amp;Ship</span> pour obtenir
une cotation de fret maritime en moins de 60 secondes.
</p>
<a
href="/dashboard"
className="inline-flex items-center justify-center space-x-2 px-6 py-3 bg-brand-turquoise text-white rounded-xl font-semibold hover:bg-brand-turquoise/90 transition-all group"
>
<span>Accéder au Dashboard</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
</motion.div>
{/* Wiki Maritime */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={isQuickAccessInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.25 }}
whileHover={{ y: -4 }}
className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8 flex flex-col"
>
<div className="w-14 h-14 bg-gradient-to-br from-brand-navy to-brand-navy/80 rounded-2xl flex items-center justify-center mb-6 flex-shrink-0">
<BookOpen className="w-7 h-7 text-brand-turquoise" />
</div>
<h3 className="text-xl font-bold text-brand-navy mb-3">Aide rapide</h3>
<p className="text-gray-600 leading-relaxed flex-1 mb-6">
Une question sur les Incoterms ou la documentation export ? Notre{' '}
<span className="font-semibold text-brand-navy">Wiki Maritime</span> contient déjà
les réponses aux questions les plus fréquentes.
</p>
<a
href="/dashboard/wiki"
className="inline-flex items-center justify-center space-x-2 px-6 py-3 border-2 border-brand-navy text-brand-navy rounded-xl font-semibold hover:bg-brand-navy hover:text-white transition-all group"
>
<span>Consulter le Wiki</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
</motion.div>
</div>
</motion.div>
</div>
</section>
{/* Map Section */} {/* Map Section */}
<section className="py-20 bg-gray-50"> <section className="py-20 bg-gray-50">
<div className="max-w-7xl mx-auto px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-6 lg:px-8">