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/
|
||||
├── 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
|
||||
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
</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 */}
|
||||
<section className="py-20 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user