From 7ff2dbb139d858884e53f544be22da8427ecda32 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 2 Jul 2026 18:15:42 -0400 Subject: [PATCH] chore: wire Carplace into dev and production stacks --- .env.docker.production.example | 5 +- Dockerfile.production | 4 +- apps/admin/next.config.js | 2 +- .../src/app/dashboard/companies/[id]/page.tsx | 12 +- apps/admin/src/app/dashboard/page.tsx | 8 +- .../src/app/dashboard/site-config/page.tsx | 64 +- apps/api/src/app.ts | 10 +- apps/api/src/data/platform-content.json | 136 +- apps/api/src/lib/emailTranslations.test.ts | 6 +- apps/api/src/lib/emailTranslations.ts | 4 +- apps/api/src/middleware/rateLimiter.ts | 2 +- apps/api/src/modules/admin/admin.routes.ts | 8 +- apps/api/src/modules/admin/admin.schemas.ts | 48 +- apps/api/src/modules/admin/admin.service.ts | 12 +- .../analytics/analytics.service.test.ts | 4 +- .../carplace.presenter.test.ts} | 4 +- .../carplace.presenter.ts} | 0 .../carplace.repo.edge.test.ts} | 22 +- .../carplace.repo.ts} | 32 +- .../src/modules/carplace/carplace.routes.ts | 164 ++ .../carplace.schemas.test.ts} | 12 +- .../carplace.schemas.ts} | 17 +- .../carplace.service.edge.test.ts} | 10 +- .../carplace.service.ts} | 239 ++- .../carplace.test.ts} | 134 +- .../companies/company.presenter.test.ts | 4 +- .../src/modules/companies/company.schemas.ts | 2 +- .../companies/company.service.edge.test.ts | 2 +- .../api/src/modules/companies/company.test.ts | 2 +- .../companies/settingsEntitlements.test.ts | 2 +- .../modules/companies/settingsEntitlements.ts | 16 +- .../reservation.lifecycle.service.ts | 4 +- .../reservation.presenter.boundary.test.ts | 4 +- .../reservation.presenter.test.ts | 2 +- .../reservations/reservation.presenter.ts | 4 +- .../modules/reviews/review.service.test.ts | 2 +- .../api/src/modules/reviews/review.service.ts | 2 +- .../src/modules/site/site.presenter.test.ts | 4 +- apps/api/src/modules/site/site.presenter.ts | 4 +- apps/api/src/modules/site/site.schemas.ts | 2 +- .../site/site.service.boundary.test.ts | 2 +- apps/api/src/modules/site/site.service.ts | 6 +- apps/api/src/modules/site/site.test.ts | 2 +- .../modules/storefront/storefront.routes.ts | 176 -- .../services/financialReportService.test.ts | 2 +- .../services/platformContentService.test.ts | 14 +- .../src/services/platformContentService.ts | 15 +- apps/api/src/swagger/openapi.boundary.test.ts | 2 +- apps/api/src/swagger/openapi.ts | 54 +- ...mployee-carplace-notification.api.test.ts} | 22 +- .../tests/api/public-validation.api.test.ts | 34 +- ... employee-carplace-boundaries.e2e.test.ts} | 4 +- .../e2e/public-validation-smoke.e2e.test.ts | 10 +- ...lling-carplace-vehicle-boundaries.test.ts} | 4 +- ...-carplace-notification-boundaries.test.ts} | 6 +- .../public-validation-boundaries.test.ts | 8 +- apps/carplace/.env.example | 15 + apps/{storefront => carplace}/next-env.d.ts | 0 apps/carplace/next.config.js | 37 + apps/{storefront => carplace}/package.json | 2 +- .../postcss.config.js | 0 .../public/favicon.ico | Bin apps/{storefront => carplace}/public/icon.svg | 0 .../public/rentalcardrive.png | Bin .../public/rentaldrivego.jpeg | Bin apps/carplace/public/rentaldrivego.png | Bin 0 -> 923628 bytes .../carplace/src/app/(public)/[slug]/page.tsx | 102 ++ .../(public)/[slug]/vehicles/[id]/page.tsx | 111 ++ .../src/app/(public)/app-policy-pages.test.ts | 2 +- .../src/app/(public)/app-privacy-ar/page.tsx | 0 .../src/app/(public)/app-privacy-en/page.tsx | 0 .../src/app/(public)/app-privacy-fr/page.tsx | 0 .../src/app/(public)/app-tc-ar/page.tsx | 0 .../src/app/(public)/app-tc-en/page.tsx | 0 .../src/app/(public)/app-tc-fr/page.tsx | 0 .../app/(public)/footer/[slug]/page.test.ts | 2 +- .../src/app/(public)/footer/[slug]/page.tsx | 0 apps/carplace/src/app/(public)/layout.tsx | 24 + apps/carplace/src/app/(public)/page.tsx | 158 ++ .../app/(public)/request/[reference]/page.tsx | 6 + .../carplace/src/app/(public)/search/page.tsx | 116 ++ apps/carplace/src/app/globals.css | 240 +++ apps/carplace/src/app/layout.tsx | 52 + .../components/CarplaceShell.content.test.ts} | 16 +- .../carplace/src/components/CarplaceShell.tsx | 213 +++ .../src/components/FooterContentPage.test.ts | 6 +- .../src/components/FooterContentPage.tsx | 8 +- .../components/carplace/CarplaceFooter.tsx | 64 + .../components/carplace/CarplaceHeader.tsx | 122 ++ .../carplace/CarplaceSearchForm.tsx | 109 ++ .../carplace/ProgressiveBookingFlow.tsx | 331 ++++ .../carplace/RequestSuccessClient.tsx | 40 + .../src/components/carplace/VehicleCard.tsx | 64 + .../src/lib/api.test.ts | 22 +- apps/carplace/src/lib/api.ts | 71 + .../src/lib/appUrls.test.ts | 4 +- .../src/lib/appUrls.ts | 0 apps/carplace/src/lib/carplace/messages.ts | 127 ++ apps/carplace/src/lib/carplace/offers.ts | 18 + apps/carplace/src/lib/carplace/routes.ts | 19 + apps/carplace/src/lib/carplace/types.ts | 113 ++ .../src/lib/footerContent.test.ts | 22 +- .../src/lib/footerContent.ts | 46 +- apps/carplace/src/lib/i18n.server.test.ts | 30 + apps/carplace/src/lib/i18n.server.ts | 7 + apps/carplace/src/lib/i18n.test.ts | 18 + apps/carplace/src/lib/i18n.ts | 7 + .../src/lib/preferences.test.ts | 38 +- .../src/lib/preferences.ts | 9 +- .../src/lib/renter.test.ts | 0 .../src/lib/renter.ts | 0 apps/carplace/src/middleware.test.ts | 53 + apps/carplace/src/proxy.ts | 47 + .../tailwind.config.ts | 0 apps/{storefront => carplace}/tsconfig.json | 0 .../{storefront => carplace}/vitest.config.ts | 0 apps/dashboard/.env.example | 17 + apps/dashboard/README.md | 16 +- apps/dashboard/next.config.js | 4 +- .../src/app/(dashboard)/offers/page.tsx | 16 +- .../(dashboard)/online-reservations/page.tsx | 6 +- .../(dashboard)/reservations/[id]/page.tsx | 2 +- .../src/app/(dashboard)/settings/page.tsx | 30 +- .../src/app/(dashboard)/subscription/page.tsx | 4 +- .../ForgotPasswordPageClient.tsx | 4 +- apps/dashboard/src/app/onboarding/page.tsx | 14 +- .../[[...sign-in]]/SignInPageClient.tsx | 4 +- .../app/sign-up/[[...sign-up]]/SignUpForm.tsx | 4 +- .../dashboard/src/components/I18nProvider.tsx | 4 +- .../src/components/layout/PublicFooter.tsx | 56 +- .../src/components/layout/PublicHeader.tsx | 8 +- .../src/components/layout/Sidebar.tsx | 6 +- apps/dashboard/src/lib/api.ts | 4 +- apps/dashboard/src/lib/urls.test.ts | 66 +- apps/dashboard/src/lib/urls.ts | 19 +- apps/dashboard/src/middleware.test.ts | 6 +- apps/dashboard/src/middleware.ts | 14 +- apps/homepage/.env.example | 4 + apps/homepage/next.config.ts | 12 +- apps/homepage/src/proxy.ts | 12 +- apps/homepage/tests/unit/proxy.test.ts | 28 +- apps/storefront/next.config.js | 80 - .../src/app/(public)/[slug]/page.tsx | 159 -- .../(public)/[slug]/vehicles/[id]/page.tsx | 283 --- .../_components/StorefrontSearchForm.tsx | 295 ---- .../_components/StorefrontVehicleGrid.tsx | 112 -- .../app/(public)/company-workspace/page.tsx | 5 - apps/storefront/src/app/(public)/layout.tsx | 41 - apps/storefront/src/app/(public)/page.tsx | 329 ---- apps/storefront/src/app/globals.css | 108 -- apps/storefront/src/app/layout.tsx | 41 - .../src/app/renter/dashboard/page.tsx | 304 ---- apps/storefront/src/app/renter/layout.tsx | 5 - .../src/app/renter/notifications/page.tsx | 153 -- .../src/app/renter/profile/page.tsx | 318 ---- .../src/app/renter/saved-companies/page.tsx | 103 -- .../src/app/renter/sign-in/page.tsx | 39 - .../src/app/renter/sign-up/page.tsx | 39 - apps/storefront/src/app/sign-in/page.tsx | 40 - .../components/BookingForm.boundary.test.ts | 29 - .../storefront/src/components/BookingForm.tsx | 721 -------- .../storefront/src/components/RenterShell.tsx | 223 --- .../src/components/StorefrontFooter.tsx | 123 -- .../StorefrontHeader.boundary.test.ts | 32 - .../src/components/StorefrontHeader.tsx | 231 --- .../components/StorefrontShell.auth.test.ts | 41 - .../src/components/StorefrontShell.tsx | 364 ---- .../WorkspaceFrame.boundary.test.ts | 48 - .../src/components/WorkspaceFrame.tsx | 111 -- apps/storefront/src/lib/api.ts | 50 - apps/storefront/src/lib/i18n.server.test.ts | 41 - apps/storefront/src/lib/i18n.server.ts | 10 - apps/storefront/src/lib/i18n.test.ts | 18 - apps/storefront/src/lib/i18n.ts | 8 - apps/storefront/src/middleware.test.ts | 169 -- apps/storefront/src/proxy.ts | 97 - docker-compose.dev.yml | 12 +- docker-compose.production.yml | 33 +- docs/DOCKER.md | 16 +- ...ALDRIVEGO_CARPLACE_AND_AUTH_FIX_PLAN(1).md | 1563 +++++++++++++++++ package-lock.json | 244 +-- package.json | 9 +- .../00000000000000_init/migration.sql | 8 +- .../20260511000000_review_token/migration.sql | 2 +- .../migration.sql | 4 +- .../migration.sql | 30 + .../migration.sql | 30 - packages/database/prisma/schema.prisma | 12 +- packages/types/src/api.ts | 4 +- ...front-homepage.ts => carplace-homepage.ts} | 70 +- packages/types/src/index.ts | 2 +- production/.env.docker.production.example | 2 +- production/README.md | 2 +- production/deploy-production.sh | 4 +- production/docker-compose.production.yml | 32 +- production/scripts/backup-restore-guide.md | 2 +- production/scripts/docker-prod-common.sh | 4 +- production/scripts/docker-prod-deploy.sh | 4 +- production/scripts/docker-prod-restore.sh | 2 +- .../scripts/docker-prod-run-pulled-image.sh | 4 +- production/scripts/docker-prod-up-all.sh | 6 +- production/scripts/docker-prod-up-carplace.sh | 11 + .../scripts/docker-prod-up-frontends.sh | 4 +- .../scripts/docker-prod-up-storefront.sh | 6 +- scripts/backup-restore-guide.md | 2 +- scripts/docker-prod-common.sh | 4 +- scripts/docker-prod-deploy.sh | 4 +- scripts/docker-prod-restore.sh | 2 +- scripts/docker-prod-run-pulled-image.sh | 4 +- scripts/docker-prod-up-all.sh | 6 +- scripts/docker-prod-up-carplace.sh | 11 + scripts/docker-prod-up-frontends.sh | 4 +- scripts/docker-prod-up-storefront.sh | 6 +- scripts/validate-phase16-design.mjs | 2 +- 214 files changed, 5258 insertions(+), 5906 deletions(-) rename apps/api/src/modules/{storefront/storefront.presenter.test.ts => carplace/carplace.presenter.test.ts} (86%) rename apps/api/src/modules/{storefront/storefront.presenter.ts => carplace/carplace.presenter.ts} (100%) rename apps/api/src/modules/{storefront/storefront.repo.edge.test.ts => carplace/carplace.repo.edge.test.ts} (84%) rename apps/api/src/modules/{storefront/storefront.repo.ts => carplace/carplace.repo.ts} (85%) create mode 100644 apps/api/src/modules/carplace/carplace.routes.ts rename apps/api/src/modules/{storefront/storefront.schemas.test.ts => carplace/carplace.schemas.test.ts} (73%) rename apps/api/src/modules/{storefront/storefront.schemas.ts => carplace/carplace.schemas.ts} (85%) rename apps/api/src/modules/{storefront/storefront.service.edge.test.ts => carplace/carplace.service.edge.test.ts} (94%) rename apps/api/src/modules/{storefront/storefront.service.ts => carplace/carplace.service.ts} (57%) rename apps/api/src/modules/{storefront/storefront.test.ts => carplace/carplace.test.ts} (62%) delete mode 100644 apps/api/src/modules/storefront/storefront.routes.ts rename apps/api/src/tests/api/{employee-storefront-notification.api.test.ts => employee-carplace-notification.api.test.ts} (82%) rename apps/api/src/tests/e2e/{employee-storefront-boundaries.e2e.test.ts => employee-carplace-boundaries.e2e.test.ts} (82%) rename apps/api/src/tests/integration/{billing-storefront-vehicle-boundaries.test.ts => billing-carplace-vehicle-boundaries.test.ts} (69%) rename apps/api/src/tests/integration/{employee-storefront-notification-boundaries.test.ts => employee-carplace-notification-boundaries.test.ts} (67%) create mode 100644 apps/carplace/.env.example rename apps/{storefront => carplace}/next-env.d.ts (100%) create mode 100644 apps/carplace/next.config.js rename apps/{storefront => carplace}/package.json (96%) rename apps/{storefront => carplace}/postcss.config.js (100%) rename apps/{storefront => carplace}/public/favicon.ico (100%) rename apps/{storefront => carplace}/public/icon.svg (100%) rename apps/{storefront => carplace}/public/rentalcardrive.png (100%) rename apps/{storefront => carplace}/public/rentaldrivego.jpeg (100%) create mode 100644 apps/carplace/public/rentaldrivego.png create mode 100644 apps/carplace/src/app/(public)/[slug]/page.tsx create mode 100644 apps/carplace/src/app/(public)/[slug]/vehicles/[id]/page.tsx rename apps/{storefront => carplace}/src/app/(public)/app-policy-pages.test.ts (96%) rename apps/{storefront => carplace}/src/app/(public)/app-privacy-ar/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/app-privacy-en/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/app-privacy-fr/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/app-tc-ar/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/app-tc-en/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/app-tc-fr/page.tsx (100%) rename apps/{storefront => carplace}/src/app/(public)/footer/[slug]/page.test.ts (96%) rename apps/{storefront => carplace}/src/app/(public)/footer/[slug]/page.tsx (100%) create mode 100644 apps/carplace/src/app/(public)/layout.tsx create mode 100644 apps/carplace/src/app/(public)/page.tsx create mode 100644 apps/carplace/src/app/(public)/request/[reference]/page.tsx create mode 100644 apps/carplace/src/app/(public)/search/page.tsx create mode 100644 apps/carplace/src/app/globals.css create mode 100644 apps/carplace/src/app/layout.tsx rename apps/{storefront/src/components/StorefrontShell.content.test.ts => carplace/src/components/CarplaceShell.content.test.ts} (74%) create mode 100644 apps/carplace/src/components/CarplaceShell.tsx rename apps/{storefront => carplace}/src/components/FooterContentPage.test.ts (91%) rename apps/{storefront => carplace}/src/components/FooterContentPage.tsx (94%) create mode 100644 apps/carplace/src/components/carplace/CarplaceFooter.tsx create mode 100644 apps/carplace/src/components/carplace/CarplaceHeader.tsx create mode 100644 apps/carplace/src/components/carplace/CarplaceSearchForm.tsx create mode 100644 apps/carplace/src/components/carplace/ProgressiveBookingFlow.tsx create mode 100644 apps/carplace/src/components/carplace/RequestSuccessClient.tsx create mode 100644 apps/carplace/src/components/carplace/VehicleCard.tsx rename apps/{storefront => carplace}/src/lib/api.test.ts (74%) create mode 100644 apps/carplace/src/lib/api.ts rename apps/{storefront => carplace}/src/lib/appUrls.test.ts (90%) rename apps/{storefront => carplace}/src/lib/appUrls.ts (100%) create mode 100644 apps/carplace/src/lib/carplace/messages.ts create mode 100644 apps/carplace/src/lib/carplace/offers.ts create mode 100644 apps/carplace/src/lib/carplace/routes.ts create mode 100644 apps/carplace/src/lib/carplace/types.ts rename apps/{storefront => carplace}/src/lib/footerContent.test.ts (79%) rename apps/{storefront => carplace}/src/lib/footerContent.ts (97%) create mode 100644 apps/carplace/src/lib/i18n.server.test.ts create mode 100644 apps/carplace/src/lib/i18n.server.ts create mode 100644 apps/carplace/src/lib/i18n.test.ts create mode 100644 apps/carplace/src/lib/i18n.ts rename apps/{storefront => carplace}/src/lib/preferences.test.ts (52%) rename apps/{storefront => carplace}/src/lib/preferences.ts (91%) rename apps/{storefront => carplace}/src/lib/renter.test.ts (100%) rename apps/{storefront => carplace}/src/lib/renter.ts (100%) create mode 100644 apps/carplace/src/middleware.test.ts create mode 100644 apps/carplace/src/proxy.ts rename apps/{storefront => carplace}/tailwind.config.ts (100%) rename apps/{storefront => carplace}/tsconfig.json (100%) rename apps/{storefront => carplace}/vitest.config.ts (100%) create mode 100644 apps/dashboard/.env.example create mode 100644 apps/homepage/.env.example delete mode 100644 apps/storefront/next.config.js delete mode 100644 apps/storefront/src/app/(public)/[slug]/page.tsx delete mode 100644 apps/storefront/src/app/(public)/[slug]/vehicles/[id]/page.tsx delete mode 100644 apps/storefront/src/app/(public)/_components/StorefrontSearchForm.tsx delete mode 100644 apps/storefront/src/app/(public)/_components/StorefrontVehicleGrid.tsx delete mode 100644 apps/storefront/src/app/(public)/company-workspace/page.tsx delete mode 100644 apps/storefront/src/app/(public)/layout.tsx delete mode 100644 apps/storefront/src/app/(public)/page.tsx delete mode 100644 apps/storefront/src/app/globals.css delete mode 100644 apps/storefront/src/app/layout.tsx delete mode 100644 apps/storefront/src/app/renter/dashboard/page.tsx delete mode 100644 apps/storefront/src/app/renter/layout.tsx delete mode 100644 apps/storefront/src/app/renter/notifications/page.tsx delete mode 100644 apps/storefront/src/app/renter/profile/page.tsx delete mode 100644 apps/storefront/src/app/renter/saved-companies/page.tsx delete mode 100644 apps/storefront/src/app/renter/sign-in/page.tsx delete mode 100644 apps/storefront/src/app/renter/sign-up/page.tsx delete mode 100644 apps/storefront/src/app/sign-in/page.tsx delete mode 100644 apps/storefront/src/components/BookingForm.boundary.test.ts delete mode 100644 apps/storefront/src/components/BookingForm.tsx delete mode 100644 apps/storefront/src/components/RenterShell.tsx delete mode 100644 apps/storefront/src/components/StorefrontFooter.tsx delete mode 100644 apps/storefront/src/components/StorefrontHeader.boundary.test.ts delete mode 100644 apps/storefront/src/components/StorefrontHeader.tsx delete mode 100644 apps/storefront/src/components/StorefrontShell.auth.test.ts delete mode 100644 apps/storefront/src/components/StorefrontShell.tsx delete mode 100644 apps/storefront/src/components/WorkspaceFrame.boundary.test.ts delete mode 100644 apps/storefront/src/components/WorkspaceFrame.tsx delete mode 100644 apps/storefront/src/lib/api.ts delete mode 100644 apps/storefront/src/lib/i18n.server.test.ts delete mode 100644 apps/storefront/src/lib/i18n.server.ts delete mode 100644 apps/storefront/src/lib/i18n.test.ts delete mode 100644 apps/storefront/src/lib/i18n.ts delete mode 100644 apps/storefront/src/middleware.test.ts delete mode 100644 apps/storefront/src/proxy.ts create mode 100644 docs/RENTALDRIVEGO_CARPLACE_AND_AUTH_FIX_PLAN(1).md create mode 100644 packages/database/prisma/migrations/20260611110000_carplace_booking_flow/migration.sql delete mode 100644 packages/database/prisma/migrations/20260611110000_marketplace_booking_flow/migration.sql rename packages/types/src/{storefront-homepage.ts => carplace-homepage.ts} (90%) create mode 100644 production/scripts/docker-prod-up-carplace.sh create mode 100644 scripts/docker-prod-up-carplace.sh diff --git a/.env.docker.production.example b/.env.docker.production.example index a6ba282..b5fef70 100644 --- a/.env.docker.production.example +++ b/.env.docker.production.example @@ -49,11 +49,10 @@ NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1 # Required for production builds. Must be an absolute https:// URL with no trailing slash. SITE_ORIGIN=https://rentaldrivego.ma -# Marketplace/storefront public routes sit at the domain root. -# Traefik routes /explore, /renter, /sign-in, /footer, and app policy pages to the storefront app. +# Carplace public routes sit under /carplace on the public site domain. NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma -NEXT_PUBLIC_STOREFRONT_URL=https://rentaldrivego.ma +NEXT_PUBLIC_CARPLACE_URL=https://rentaldrivego.ma/carplace NEXT_PUBLIC_DASHBOARD_URL=https://rentaldrivego.ma/dashboard NEXT_PUBLIC_ADMIN_URL=https://rentaldrivego.ma/admin NEXT_PUBLIC_HOMEPAGE_URL=https://rentaldrivego.ma diff --git a/Dockerfile.production b/Dockerfile.production index 28ad0be..f1a957c 100644 --- a/Dockerfile.production +++ b/Dockerfile.production @@ -18,7 +18,7 @@ ARG ADMIN_INTERNAL_URL=http://admin:3002 # NEXT_PUBLIC_* vars must be present at build time — they are inlined into JS bundles ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_HOMEPAGE_URL -ARG NEXT_PUBLIC_STOREFRONT_URL +ARG NEXT_PUBLIC_CARPLACE_URL ARG NEXT_PUBLIC_DASHBOARD_URL ARG NEXT_PUBLIC_ADMIN_URL @@ -31,7 +31,7 @@ ENV API_INTERNAL_URL=$API_INTERNAL_URL \ ADMIN_INTERNAL_URL=$ADMIN_INTERNAL_URL \ NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ NEXT_PUBLIC_HOMEPAGE_URL=$NEXT_PUBLIC_HOMEPAGE_URL \ - NEXT_PUBLIC_STOREFRONT_URL=$NEXT_PUBLIC_STOREFRONT_URL \ + NEXT_PUBLIC_CARPLACE_URL=$NEXT_PUBLIC_CARPLACE_URL \ NEXT_PUBLIC_DASHBOARD_URL=$NEXT_PUBLIC_DASHBOARD_URL \ NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \ SITE_ORIGIN=$SITE_ORIGIN diff --git a/apps/admin/next.config.js b/apps/admin/next.config.js index 80b7b8f..c3666a9 100644 --- a/apps/admin/next.config.js +++ b/apps/admin/next.config.js @@ -5,7 +5,7 @@ const apiOrigin = (process.env.API_INTERNAL_URL ?? process.env.API_URL ?? 'http: const apiUrl = new URL(apiOrigin) const ADMIN_BASE_PATH = '/admin' -// In Docker dev the admin app runs on port 3002 while the storefront proxy +// In Docker dev the admin app runs on port 3002 while the Carplace proxy // serves it from port 3000. When ADMIN_ASSET_PREFIX is set, Next emits // absolute chunk URLs so /admin pages load their JS/CSS and HMR directly from // port 3002, bypassing the proxy (which can't upgrade WebSocket connections). diff --git a/apps/admin/src/app/dashboard/companies/[id]/page.tsx b/apps/admin/src/app/dashboard/companies/[id]/page.tsx index c068a5a..cb45340 100644 --- a/apps/admin/src/app/dashboard/companies/[id]/page.tsx +++ b/apps/admin/src/app/dashboard/companies/[id]/page.tsx @@ -71,7 +71,7 @@ interface CompanyDetail { whatsappNumber: string | null defaultLocale: string defaultCurrency: string - isListedOnMarketplace: boolean + isListedOnCarplace: boolean } | null contractSettings: { legalName: string | null @@ -171,7 +171,7 @@ interface FormState { whatsappNumber: string defaultLocale: string defaultCurrency: string - isListedOnMarketplace: boolean + isListedOnCarplace: boolean } contractSettings: { legalName: string @@ -277,7 +277,7 @@ function createFormState(company: CompanyDetail): FormState { whatsappNumber: company.brand?.whatsappNumber ?? '', defaultLocale: company.brand?.defaultLocale ?? 'en', defaultCurrency: company.brand?.defaultCurrency ?? 'MAD', - isListedOnMarketplace: company.brand?.isListedOnMarketplace ?? true, + isListedOnCarplace: company.brand?.isListedOnCarplace ?? true, }, contractSettings: { legalName: company.contractSettings?.legalName ?? '', @@ -420,7 +420,7 @@ export default function AdminCompanyDetailPage() { whatsappNumber: toNullable(form.brand.whatsappNumber), defaultLocale: form.brand.defaultLocale, defaultCurrency: form.brand.defaultCurrency, - isListedOnMarketplace: form.brand.isListedOnMarketplace, + isListedOnCarplace: form.brand.isListedOnCarplace, }, contractSettings: { legalName: toNullable(form.contractSettings.legalName), @@ -723,8 +723,8 @@ export default function AdminCompanyDetailPage() { updateSection('brand', { publicAddress: e.target.value })} /> diff --git a/apps/admin/src/app/dashboard/page.tsx b/apps/admin/src/app/dashboard/page.tsx index ae5f740..52ddbbd 100644 --- a/apps/admin/src/app/dashboard/page.tsx +++ b/apps/admin/src/app/dashboard/page.tsx @@ -24,7 +24,7 @@ export default function AdminDashboardPage() { brand: 'RentalDriveGo', eyebrow: 'Operations command', platformOverview: 'RentalDriveGo admin dashboard', - subtitle: 'Monitor storefront health, subscription coverage, renter activity, and operator actions from one control surface.', + subtitle: 'Monitor Carplace health, subscription coverage, renter activity, and operator actions from one control surface.', liveSignal: 'Live platform signal', readiness: 'Operational readiness', readinessBody: 'Core admin workflows are connected and ready for platform support.', @@ -36,14 +36,14 @@ export default function AdminDashboardPage() { ['Renters', 'Support renter trust workflows, blocking, and account recovery.', 'View all'], ['Audit logs', 'Trace every sensitive admin action across RentalDriveGo.', 'View logs'], ], - health: ['Tenant accounts', 'Storefront identity', 'Admin audit trail'], + health: ['Tenant accounts', 'Carplace identity', 'Admin audit trail'], }, fr: { kpis: ['Entreprises totales', 'Entreprises actives', 'Locataires totaux', 'Réservations totales'], brand: 'RentalDriveGo', eyebrow: 'Centre des opérations', platformOverview: 'Tableau de bord admin RentalDriveGo', - subtitle: 'Suivez la santé de la storefront, les abonnements, l’activité des locataires et les actions opérateur depuis une même interface.', + subtitle: 'Suivez la santé de la Carplace, les abonnements, l’activité des locataires et les actions opérateur depuis une même interface.', liveSignal: 'Signal plateforme en direct', readiness: 'Disponibilité opérationnelle', readinessBody: 'Les principaux workflows admin sont connectés et prêts pour le support plateforme.', @@ -55,7 +55,7 @@ export default function AdminDashboardPage() { ['Locataires', 'Gérer les workflows de confiance, de blocage et de récupération.', 'Voir tout'], ['Journaux d’audit', 'Tracer chaque action admin sensible dans RentalDriveGo.', 'Voir les journaux'], ], - health: ['Comptes locataires', 'Identité storefront', 'Piste d’audit admin'], + health: ['Comptes locataires', 'Identité Carplace', 'Piste d’audit admin'], }, ar: { kpis: ['إجمالي الشركات', 'الشركات النشطة', 'إجمالي المستأجرين', 'إجمالي الحجوزات'], diff --git a/apps/admin/src/app/dashboard/site-config/page.tsx b/apps/admin/src/app/dashboard/site-config/page.tsx index 99d6ffd..f55fd6c 100644 --- a/apps/admin/src/app/dashboard/site-config/page.tsx +++ b/apps/admin/src/app/dashboard/site-config/page.tsx @@ -3,12 +3,12 @@ import { useEffect, useState } from 'react' import Link from 'next/link' import { - cloneStorefrontHomepageContent, - resolveStorefrontHomepageSections, - type StorefrontHomepageConfig, - type StorefrontHomepageContent, - type StorefrontHomepageSectionType, - type StorefrontLanguage, + cloneCarplaceHomepageContent, + resolveCarplaceHomepageSections, + type CarplaceHomepageConfig, + type CarplaceHomepageContent, + type CarplaceHomepageSectionType, + type CarplaceLanguage, } from '@rentaldrivego/types' import { useAdminI18n } from '@/components/I18nProvider' import { ADMIN_API_BASE } from '@/lib/api' @@ -35,15 +35,15 @@ const STATUS_COLORS: Record = { const INPUT_CLASS = 'w-full rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500' const LABEL_CLASS = 'mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-zinc-500' -function cloneHomepageContent(content: StorefrontHomepageConfig) { - const cloned = JSON.parse(JSON.stringify(content)) as StorefrontHomepageConfig - ;(['en', 'fr', 'ar'] as StorefrontLanguage[]).forEach((language) => { - cloned[language].sections = resolveStorefrontHomepageSections(cloned[language].sections) +function cloneHomepageContent(content: CarplaceHomepageConfig) { + const cloned = JSON.parse(JSON.stringify(content)) as CarplaceHomepageConfig + ;(['en', 'fr', 'ar'] as CarplaceLanguage[]).forEach((language) => { + cloned[language].sections = resolveCarplaceHomepageSections(cloned[language].sections) }) return cloned } -const HOMEPAGE_SECTIONS: StorefrontHomepageSectionType[] = [ +const HOMEPAGE_SECTIONS: CarplaceHomepageSectionType[] = [ 'hero', 'surface', 'pillars', @@ -58,12 +58,12 @@ export default function AdminSiteConfigPage() { const copy = { en: { title: 'Site configuration', - description: 'Edit the main storefront homepage here, or jump into a company to manage its branded public homepage and menu.', + description: 'Edit the main Carplace homepage here, or jump into a company to manage its branded public homepage and menu.', homepageTitle: 'Main website homepage', - homepageDescription: 'This controls the storefront homepage shown on the main RentalDriveGo website.', + homepageDescription: 'This controls the Carplace homepage shown on the main RentalDriveGo website.', saveHomepage: 'Save homepage', savingHomepage: 'Saving…', - homepageSaved: 'Storefront homepage saved.', + homepageSaved: 'Carplace homepage saved.', search: 'Search by company or slug…', loading: 'Loading…', empty: 'No companies found', @@ -86,7 +86,7 @@ export default function AdminSiteConfigPage() { previewTitle: 'Live preview', previewDescription: 'Draft changes appear here before you save them.', homepageSections: 'Homepage sections', - homepageSectionsDescription: 'Add or remove blocks from the main storefront homepage.', + homepageSectionsDescription: 'Add or remove blocks from the main Carplace homepage.', addSection: 'Add', removeSection: 'Remove', expand: 'Expand', @@ -94,12 +94,12 @@ export default function AdminSiteConfigPage() { }, fr: { title: 'Configuration du site', - description: 'Modifiez ici la page d’accueil principale de la storefront, ou ouvrez une entreprise pour gérer sa page publique et son menu.', + description: 'Modifiez ici la page d’accueil principale de la Carplace, ou ouvrez une entreprise pour gérer sa page publique et son menu.', homepageTitle: 'Page d’accueil du site principal', - homepageDescription: 'Cette section contrôle la page d’accueil storefront affichée sur le site principal de RentalDriveGo.', + homepageDescription: 'Cette section contrôle la page d’accueil Carplace affichée sur le site principal de RentalDriveGo.', saveHomepage: 'Enregistrer la page d’accueil', savingHomepage: 'Enregistrement…', - homepageSaved: 'Page d’accueil storefront enregistrée.', + homepageSaved: 'Page d’accueil Carplace enregistrée.', search: 'Rechercher par entreprise ou slug…', loading: 'Chargement…', empty: 'Aucune entreprise trouvée', @@ -112,7 +112,7 @@ export default function AdminSiteConfigPage() { companiesTitle: 'Sites de marque des entreprises', languageLabel: 'Langue', hero: 'Bloc principal', - surface: 'Bloc storefront', + surface: 'Bloc Carplace', companySection: 'Bloc opérateur', renterSection: 'Bloc client', valueSection: 'Blocs de valeur', @@ -122,7 +122,7 @@ export default function AdminSiteConfigPage() { previewTitle: 'Aperçu en direct', previewDescription: 'Les brouillons apparaissent ici avant l’enregistrement.', homepageSections: 'Sections de la page d’accueil', - homepageSectionsDescription: 'Ajoutez ou retirez des blocs de la page d’accueil storefront principale.', + homepageSectionsDescription: 'Ajoutez ou retirez des blocs de la page d’accueil Carplace principale.', addSection: 'Ajouter', removeSection: 'Retirer', expand: 'Ouvrir', @@ -171,8 +171,8 @@ export default function AdminSiteConfigPage() { const [search, setSearch] = useState('') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const [homepage, setHomepage] = useState(cloneStorefrontHomepageContent()) - const [homepageLanguage, setHomepageLanguage] = useState(language) + const [homepage, setHomepage] = useState(cloneCarplaceHomepageContent()) + const [homepageLanguage, setHomepageLanguage] = useState(language) const [homepageSaving, setHomepageSaving] = useState(false) const [homepageMessage, setHomepageMessage] = useState(null) const [homepageExpanded, setHomepageExpanded] = useState(false) @@ -189,7 +189,7 @@ export default function AdminSiteConfigPage() { credentials: 'include', cache: 'no-store', }), - fetch(`${ADMIN_API_BASE}/admin/site-config/storefront-homepage`, { + fetch(`${ADMIN_API_BASE}/admin/site-config/carplace-homepage`, { credentials: 'include', cache: 'no-store', }), @@ -202,7 +202,7 @@ export default function AdminSiteConfigPage() { const homepageJson = await homepageRes.json() if (homepageRes.ok && homepageJson?.data) { - setHomepage(cloneHomepageContent(homepageJson.data as StorefrontHomepageConfig)) + setHomepage(cloneHomepageContent(homepageJson.data as CarplaceHomepageConfig)) } } catch (err: any) { setError(err.message) @@ -225,7 +225,7 @@ export default function AdminSiteConfigPage() { ) }, [search, companies]) - function updateHomepageContent(patch: Partial) { + function updateHomepageContent(patch: Partial) { setHomepage((current) => ({ ...current, [homepageLanguage]: { @@ -257,16 +257,16 @@ export default function AdminSiteConfigPage() { } function getActiveSections() { - return resolveStorefrontHomepageSections(activeContent.sections) + return resolveCarplaceHomepageSections(activeContent.sections) } - function addHomepageSection(section: StorefrontHomepageSectionType) { + function addHomepageSection(section: CarplaceHomepageSectionType) { const sections = getActiveSections() if (sections.includes(section)) return updateHomepageContent({ sections: [...sections, section] }) } - function removeHomepageSection(section: StorefrontHomepageSectionType) { + function removeHomepageSection(section: CarplaceHomepageSectionType) { const sections = getActiveSections().filter((item) => item !== section) if (sections.length === 0) return updateHomepageContent({ sections }) @@ -276,7 +276,7 @@ export default function AdminSiteConfigPage() { setHomepageSaving(true) setHomepageMessage(null) try { - const res = await fetch(`${ADMIN_API_BASE}/admin/site-config/storefront-homepage`, { + const res = await fetch(`${ADMIN_API_BASE}/admin/site-config/carplace-homepage`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -286,7 +286,7 @@ export default function AdminSiteConfigPage() { }) const json = await res.json() if (!res.ok) throw new Error(json?.message ?? 'Failed to save homepage') - setHomepage(cloneHomepageContent(json.data as StorefrontHomepageConfig)) + setHomepage(cloneHomepageContent(json.data as CarplaceHomepageConfig)) setHomepageMessage(copy.homepageSaved) } catch (err: any) { setError(err.message) @@ -298,7 +298,7 @@ export default function AdminSiteConfigPage() { const activeContent = homepage[homepageLanguage] const activeSections = getActiveSections() const availableSections = HOMEPAGE_SECTIONS.filter((section) => !activeSections.includes(section)) - const sectionLabels: Record = { + const sectionLabels: Record = { hero: copy.hero, surface: copy.surface, pillars: copy.valueSection, @@ -337,7 +337,7 @@ export default function AdminSiteConfigPage() {
{copy.languageLabel} - {(['en', 'fr', 'ar'] as StorefrontLanguage[]).map((value) => ( + {(['en', 'fr', 'ar'] as CarplaceLanguage[]).map((value) => ( + {copy.results.clear} + + + +
+
+

Carplace

{copy.results.title}

{response.pagination.totalItems} {copy.results.count}

+
+ {Object.entries(params).flatMap(([key, raw]) => key === 'sort' || key === 'page' ? [] : (Array.isArray(raw) ? raw : [raw]).filter((entry): entry is string => typeof entry === 'string').map((entry) => ))} + + +
+
+ + {items.length > 0 ?
{items.map((vehicle) => )}
:

{copy.results.none}

{copy.results.noneBody}

} + + {response.pagination.totalPages > 1 ? : null} +
+
+ + ) +} + +function toIso(date?: string, time?: string): string | null { if (!date || !time) return null; const result = new Date(`${date}T${time}:00`); return Number.isNaN(result.getTime()) ? null : result.toISOString() } + +function buildCardQuery(params: RawSearchParams): URLSearchParams { + const query = new URLSearchParams() + for (const key of ['pickupLocation', 'dropoffLocation', 'dropoffMode', 'pickupDate', 'pickupTime', 'returnDate', 'returnTime', 'driverAge', 'promoCode'] as const) { + const raw = params[key] + if (typeof raw === 'string' && raw) query.set(key, raw) + } + return query +} + +function preservedTripInputs(params: RawSearchParams) { + return ['pickupLocation', 'dropoffLocation', 'dropoffMode', 'pickupDate', 'pickupTime', 'returnDate', 'returnTime', 'driverAge', 'promoCode'].flatMap((key) => { + const raw = params[key] + return (Array.isArray(raw) ? raw : [raw]).filter((entry): entry is string => typeof entry === 'string').map((entry) => ) + }) +} + +function Pagination({ current, total, params, previous, next, label }: { current: number; total: number; params: RawSearchParams; previous: string; next: string; label: string }) { + const href = (page: number) => { const query = new URLSearchParams(); Object.entries(params).forEach(([key, raw]) => { if (key === 'page') return; (Array.isArray(raw) ? raw : [raw]).forEach((entry) => { if (typeof entry === 'string') query.append(key, entry) }) }); query.set('page', String(page)); return `${carplaceHref('/search')}?${query.toString()}` } + return +} diff --git a/apps/carplace/src/app/globals.css b/apps/carplace/src/app/globals.css new file mode 100644 index 0000000..3b18d95 --- /dev/null +++ b/apps/carplace/src/app/globals.css @@ -0,0 +1,240 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-white text-blue-950 antialiased transition-colors dark:text-slate-100; +} + +html.dark body { + background-image: + linear-gradient(180deg, #172554 0%, #1b3068 35%, #0f1a40 100%); +} + +.shell { + @apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8; +} + +.card { + @apply rounded-[2rem] border shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:shadow-[0_30px_80px_rgba(0,0,0,0.36)]; + border-color: rgb(231 229 228 / 0.8); + background-color: rgb(255 255 255 / 0.82); +} + +html.dark .card { + border-color: rgb(30 64 175 / 0.40); + background-color: rgb(23 37 84 / 0.72); +} + +.site-page { + @apply min-h-screen overflow-hidden text-blue-950; + background-image: + linear-gradient(180deg, #ffffff 0%, #fafafa 28%, #f5f5f5 58%, #ffffff 100%); +} + +html.dark .site-page { + @apply text-slate-100; + background-image: + linear-gradient(180deg, #172554 0%, #1b3068 35%, #0f1a40 100%); +} + +.site-glow { + background-image: + radial-gradient(circle at top left, rgba(234, 88, 12, 0.24), transparent 34%), + radial-gradient(circle at top right, rgba(59, 130, 246, 0.18), transparent 26%); +} + +html.dark .site-glow { + background-image: + radial-gradient(circle at top left, rgba(251, 146, 60, 0.22), transparent 34%), + radial-gradient(circle at top right, rgba(30, 64, 175, 0.18), transparent 26%); +} + +.site-section { + @apply shell py-10 sm:py-14 lg:py-16; +} + +.site-panel { + @apply rounded-[2rem] border p-7 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors sm:p-8; + border-color: rgb(231 229 228 / 0.8); + background-color: rgb(255 255 255 / 0.82); +} + +html.dark .site-panel { + border-color: rgb(30 64 175 / 0.40); + background-color: rgb(23 37 84 / 0.72); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.36); +} + +.site-panel-muted { + @apply rounded-[2rem] border p-7 transition-colors sm:p-8; + border-color: rgb(231 229 228 / 0.8); + background-image: linear-gradient(160deg, #fafafa 0%, #f5f5f5 100%); +} + +html.dark .site-panel-muted { + border-color: rgb(30 64 175 / 0.40); + background-image: linear-gradient(160deg, #172554 0%, #1b3068 100%); +} + +.site-panel-contrast { + @apply rounded-[2rem] border p-7 text-white shadow-[0_30px_80px_rgba(28,25,23,0.18)] transition-colors sm:p-8; + border-color: rgb(20 45 95 / 0.8); + background-color: #172554; +} + +html.dark .site-panel-contrast { + border-color: rgb(30 64 175); +} + +.site-kicker { + @apply text-xs font-bold uppercase tracking-[0.28em] text-orange-600 dark:text-orange-400; +} + +.site-title { + @apply mt-4 text-4xl font-black tracking-[-0.04em] text-blue-950 dark:text-white sm:text-5xl; +} + +.site-lead { + @apply mt-5 text-base leading-8 text-stone-600 dark:text-slate-300 sm:text-lg; +} + +.site-link-primary { + @apply rounded-full bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400; +} + +.site-link-secondary { + @apply rounded-full border border-stone-300 bg-white/85 px-6 py-3 text-sm font-semibold text-stone-700 transition hover:border-blue-900 hover:text-blue-900 dark:border-[#2e50f1] dark:bg-[#2e50f1]/30 dark:text-[#1e3de0] dark:hover:border-[#1e3de0] dark:hover:text-white; +} + +/* Carplace public experience */ +html { + min-width: 320px; + scroll-behavior: smooth; + scroll-padding-top: 96px; +} + +html[lang='ar'] body { + font-family: 'Noto Sans Arabic', Tahoma, Arial, sans-serif; + line-height: 1.75; +} + +:focus-visible { + outline: 3px solid #f97316; + outline-offset: 3px; +} + +.skip-link { + position: fixed; + z-index: 1000; + inset-block-start: .5rem; + inset-inline-start: .5rem; + transform: translateY(-160%); + border-radius: .75rem; + background: #172554; + color: white; + padding: .75rem 1rem; + font-weight: 800; + transition: transform .15s ease; +} + +.skip-link:focus { transform: translateY(0); } + +.carplace-header { + position: sticky; + z-index: 100; + inset-block-start: 0; + border-bottom: 1px solid rgb(231 229 228 / .9); + background: rgb(255 255 255 / .94); + box-shadow: 0 1px 2px rgb(15 23 42 / .06); + backdrop-filter: blur(14px); +} + +html.dark .carplace-header { + border-color: rgb(30 64 175 / .55); + background: rgb(23 37 84 / .94); +} + +.carplace-control, +.carplace-icon-button, +.carplace-company-link, +.carplace-primary-button, +.carplace-secondary-button, +.carplace-light-button, +.carplace-orange-button { + @apply inline-flex min-h-11 items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-bold no-underline transition disabled:cursor-not-allowed disabled:opacity-60; +} + +.carplace-control, +.carplace-icon-button, +.carplace-secondary-button { + @apply border border-stone-300 bg-white text-stone-700 hover:border-blue-900 hover:text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-slate-200 dark:hover:border-blue-500 dark:hover:text-white; +} + +.carplace-icon-button { @apply min-w-11 justify-center px-0; } +.carplace-company-link { @apply bg-blue-950 text-white hover:bg-blue-900 dark:bg-orange-500 dark:hover:bg-orange-400; } +.carplace-primary-button, +.carplace-orange-button { @apply bg-orange-600 text-white hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400; } +.carplace-light-button { @apply bg-white text-blue-950 hover:bg-blue-50; } + +.carplace-menu { + @apply absolute top-full z-50 mt-2 w-60 overflow-hidden rounded-2xl border border-stone-200 bg-white p-1 shadow-2xl dark:border-blue-800 dark:bg-blue-950; +} + +.carplace-menu-item { @apply flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-start text-sm font-semibold text-stone-700 hover:bg-blue-50 dark:text-slate-200 dark:hover:bg-blue-900/50; } + +.carplace-hero { + background-image: + radial-gradient(circle at 10% 10%, rgb(249 115 22 / .16), transparent 28%), + radial-gradient(circle at 92% 8%, rgb(37 99 235 / .14), transparent 28%), + linear-gradient(180deg, #fff, #f8fafc); +} + +html.dark .carplace-hero { + background-image: + radial-gradient(circle at 10% 10%, rgb(249 115 22 / .18), transparent 28%), + radial-gradient(circle at 92% 8%, rgb(59 130 246 / .16), transparent 28%), + linear-gradient(180deg, #172554, #0f1a40); +} + +.carplace-search { + @apply rounded-[2rem] border border-stone-200 bg-white/95 p-5 shadow-[0_30px_90px_rgba(15,23,42,0.14)] backdrop-blur dark:border-blue-800 dark:bg-blue-950/95 sm:p-6; +} + +.carplace-search-compact { + @apply rounded-2xl p-4 shadow-none; +} + +.carplace-input { + @apply min-h-11 w-full rounded-xl border border-stone-300 bg-white px-3 py-2 text-sm font-medium text-blue-950 outline-none transition placeholder:text-stone-400 hover:border-blue-500 focus:border-blue-700 focus:ring-2 focus:ring-blue-200 dark:border-blue-800 dark:bg-blue-950 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-900; +} + +.carplace-choice { @apply cursor-pointer rounded-full border border-stone-300 bg-white px-3 py-2 text-xs font-bold text-stone-600 transition dark:border-blue-800 dark:bg-blue-950 dark:text-slate-300; } +.carplace-choice-active { @apply border-orange-500 bg-orange-50 text-orange-800 dark:bg-orange-950/40 dark:text-orange-200; } +.carplace-trust-pill { @apply inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white/85 px-3 py-2 shadow-sm dark:border-blue-800 dark:bg-blue-950/70; } +.carplace-dark-pill { @apply inline-flex items-center gap-2 rounded-full border border-blue-700 bg-blue-900/70 px-3 py-2; } +.carplace-spec { @apply inline-flex items-center gap-1.5 rounded-full bg-stone-100 px-3 py-1.5 dark:bg-blue-900/45; } + +.carplace-city-link { @apply inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white px-4 py-2.5 text-sm font-bold text-stone-700 no-underline shadow-sm transition hover:-translate-y-0.5 hover:border-orange-400 hover:text-orange-700 dark:border-blue-800 dark:bg-blue-950 dark:text-slate-200 dark:hover:border-orange-500 dark:hover:text-orange-300; } +.carplace-category-card, +.carplace-company-card { @apply rounded-[1.5rem] border border-stone-200 bg-white p-5 no-underline shadow-[0_14px_45px_rgba(15,23,42,0.06)] transition hover:-translate-y-1 hover:border-blue-300 hover:shadow-[0_20px_60px_rgba(15,23,42,0.12)] dark:border-blue-900 dark:bg-blue-950 dark:hover:border-blue-600; } +.carplace-offer-card { @apply rounded-[2rem] bg-gradient-to-br from-blue-950 to-blue-800 p-6 shadow-[0_24px_70px_rgba(15,23,42,0.2)]; } +.carplace-guidance { @apply flex gap-3 rounded-2xl border border-stone-200 bg-white p-5 dark:border-blue-900 dark:bg-blue-950; } +.carplace-faq { @apply rounded-2xl border border-stone-200 bg-white p-5 dark:border-blue-900 dark:bg-blue-950; } +.carplace-faq summary { @apply cursor-pointer font-black text-blue-950 dark:text-white; } +.carplace-faq p { @apply mb-0 mt-3 text-sm leading-7 text-stone-600 dark:text-slate-300; } + +.carplace-vehicle-card { @apply flex min-w-0 flex-col overflow-hidden rounded-[1.75rem] border border-stone-200 bg-white shadow-[0_18px_60px_rgba(15,23,42,0.08)] transition hover:-translate-y-1 hover:border-blue-300 hover:shadow-[0_24px_70px_rgba(15,23,42,0.14)] dark:border-blue-900 dark:bg-blue-950 dark:hover:border-blue-600; } +.carplace-card-action { @apply grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-blue-950 text-white no-underline transition hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-400; } +.carplace-booking { @apply rounded-[2rem] border border-stone-200 bg-white p-6 shadow-[0_24px_70px_rgba(15,23,42,0.12)] dark:border-blue-800 dark:bg-blue-950; } +.carplace-step { @apply flex min-w-0 flex-col items-center gap-1 text-center text-[10px] font-bold text-stone-400; } +.carplace-step span { @apply grid h-7 w-7 place-items-center rounded-full border border-stone-300 bg-white dark:border-blue-800 dark:bg-blue-950; } +.carplace-step-active { @apply text-orange-700 dark:text-orange-300; } +.carplace-step-active span { @apply border-orange-500 bg-orange-50 dark:bg-orange-950/50; } +.carplace-step-done { @apply text-emerald-700 dark:text-emerald-300; } +.carplace-step-done span { @apply border-emerald-500 bg-emerald-50 dark:bg-emerald-950/50; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; } +} diff --git a/apps/carplace/src/app/layout.tsx b/apps/carplace/src/app/layout.tsx new file mode 100644 index 0000000..58e2563 --- /dev/null +++ b/apps/carplace/src/app/layout.tsx @@ -0,0 +1,52 @@ +import type { Metadata } from 'next' +import { cookies } from 'next/headers' +import CarplaceShell from '@/components/CarplaceShell' +import { getCarplaceMessages } from '@/lib/carplace/messages' +import { getCarplaceLanguage } from '@/lib/i18n.server' +import './globals.css' + +export async function generateMetadata(): Promise { + const language = await getCarplaceLanguage() + const copy = getCarplaceMessages(language) + return { + title: `${copy.brand} by RentalDriveGo`, + description: copy.hero.body, + alternates: { canonical: '/carplace' }, + openGraph: { + title: `${copy.brand} by RentalDriveGo`, + description: copy.hero.body, + type: 'website', + locale: language === 'ar' ? 'ar_MA' : language === 'fr' ? 'fr_MA' : 'en_MA', + siteName: 'RentalDriveGo', + }, + icons: { + icon: '/rentaldrivego.png', + shortcut: '/favicon.ico', + apple: '/rentaldrivego.png', + }, + } +} + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const language = await getCarplaceLanguage() + const cookieStore = await cookies() + const rawTheme = cookieStore.get('rentaldrivego-theme')?.value + const theme = rawTheme === 'dark' ? 'dark' : 'light' + + return ( + + + {/* Runs before hydration to prevent flash of wrong theme */} +