fix contact
This commit is contained in:
parent
737d94e99a
commit
7f31aa59a9
41
CLAUDE.md
41
CLAUDE.md
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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&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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user