diff --git a/carplace/next-env.d.ts b/carplace/next-env.d.ts deleted file mode 100644 index c4b7818..0000000 --- a/carplace/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/dev/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/carplace/next.config.js b/carplace/next.config.js deleted file mode 100644 index 4967c1b..0000000 --- a/carplace/next.config.js +++ /dev/null @@ -1,74 +0,0 @@ -/** @type {import('next').NextConfig} */ - -const { buildSecurityHeaders, normalizeAssetPrefix } = require('../../config/nextSecurityHeaders') - -// The carplace proxies /dashboard and /admin to their own Next dev servers. -// Allow those asset-prefix origins so proxied pages can load chunks and HMR. -const dashboardAssetSource = normalizeAssetPrefix( - process.env.DASHBOARD_ASSET_PREFIX ?? (process.env.NODE_ENV !== 'production' ? 'http://localhost:3001' : undefined), - '/dashboard', -) -const securityHeaders = buildSecurityHeaders({ - assetSources: [dashboardAssetSource, process.env.ADMIN_ASSET_PREFIX], -}) -const nextConfig = { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'res.cloudinary.com', - }, - ], - }, - transpilePackages: ['@rentaldrivego/types'], - async headers() { - return [ - { - source: '/:path*', - headers: securityHeaders, - }, - ] - }, - async redirects() { - return [ - { - source: '/dashboard/dashboard', - destination: '/dashboard', - permanent: false, - }, - { - source: '/dashboard/dashboard/:path*', - destination: '/dashboard/:path*', - permanent: false, - }, - ] - }, - async rewrites() { - const dashboardOrigin = process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001' - const adminOrigin = process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002' - - return [ - // Dashboard has basePath '/dashboard'. The proxy preserves the prefix - // so the dashboard can strip it and route internally. - { - source: '/dashboard', - destination: `${dashboardOrigin}/dashboard`, - }, - { - source: '/dashboard/:path*', - destination: `${dashboardOrigin}/dashboard/:path*`, - }, - // Admin routes (also uses basePath) - { - source: '/admin', - destination: `${adminOrigin}/admin`, - }, - { - source: '/admin/:path*', - destination: `${adminOrigin}/admin/:path*`, - }, - ] - }, -} - -module.exports = nextConfig diff --git a/carplace/package.json b/carplace/package.json deleted file mode 100644 index 7669b19..0000000 --- a/carplace/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@rentaldrivego/carplace", - "version": "1.0.0", - "private": true, - "scripts": { - "predev": "npm run build --workspace @rentaldrivego/types", - "dev": "next dev -H 0.0.0.0 -p 3004", - "prebuild": "npm run build --workspace @rentaldrivego/types", - "build": "next build", - "prestart": "npm run build --workspace @rentaldrivego/types", - "pretype-check": "npm run build --workspace @rentaldrivego/types", - "start": "next start -H 0.0.0.0 -p 3004", - "type-check": "tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "@rentaldrivego/types": "*", - "autoprefixer": "^10.4.19", - "lucide-react": "^0.376.0", - "next": "^16.2.7", - "postcss": "^8.4.38", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwindcss": "^3.4.3" - }, - "devDependencies": { - "@types/node": "^20.12.0", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", - "typescript": "^5.4.0", - "vitest": "^1.6.0" - } -} diff --git a/carplace/postcss.config.js b/carplace/postcss.config.js deleted file mode 100644 index 33ad091..0000000 --- a/carplace/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/carplace/public/favicon.ico b/carplace/public/favicon.ico deleted file mode 100644 index 2fee055..0000000 Binary files a/carplace/public/favicon.ico and /dev/null differ diff --git a/carplace/public/icon.svg b/carplace/public/icon.svg deleted file mode 100644 index d2dee78..0000000 --- a/carplace/public/icon.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - R - - diff --git a/carplace/public/rentalcardrive.png b/carplace/public/rentalcardrive.png deleted file mode 100644 index 2fee055..0000000 Binary files a/carplace/public/rentalcardrive.png and /dev/null differ diff --git a/carplace/public/rentaldrivego.jpeg b/carplace/public/rentaldrivego.jpeg deleted file mode 100644 index 4547894..0000000 Binary files a/carplace/public/rentaldrivego.jpeg and /dev/null differ diff --git a/carplace/src/app/(public)/app-policy-pages.test.ts b/carplace/src/app/(public)/app-policy-pages.test.ts deleted file mode 100644 index 4ebe9fa..0000000 --- a/carplace/src/app/(public)/app-policy-pages.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import React, { isValidElement } from 'react' -import { describe, expect, it } from 'vitest' -import FooterContentPage from '@/components/FooterContentPage' -import AppPrivacyEnPage from './app-privacy-en/page' -import AppPrivacyFrPage from './app-privacy-fr/page' -import AppPrivacyArPage from './app-privacy-ar/page' -import AppTermsEnPage from './app-tc-en/page' -import AppTermsFrPage from './app-tc-fr/page' -import AppTermsArPage from './app-tc-ar/page' - -function expectPolicyPage(element: unknown, slug: string, forcedLanguage: string) { - expect(isValidElement(element)).toBe(true) - const page = element as React.ReactElement - expect(page.type).toBe(FooterContentPage) - expect(page.props.slug).toBe(slug) - expect(page.props.forcedLanguage).toBe(forcedLanguage) -} - -describe('carplace static app policy pages', () => { - it('binds app privacy pages to explicit locales', () => { - expectPolicyPage(AppPrivacyEnPage(), 'privacy-policy', 'en') - expectPolicyPage(AppPrivacyFrPage(), 'privacy-policy', 'fr') - expectPolicyPage(AppPrivacyArPage(), 'privacy-policy', 'ar') - }) - - it('binds app terms pages to explicit locales and the app terms content slug', () => { - expectPolicyPage(AppTermsEnPage(), 'general-conditions', 'en') - expectPolicyPage(AppTermsFrPage(), 'general-conditions', 'fr') - expectPolicyPage(AppTermsArPage(), 'general-conditions', 'ar') - }) -}) diff --git a/carplace/src/app/(public)/app-privacy-ar/page.tsx b/carplace/src/app/(public)/app-privacy-ar/page.tsx deleted file mode 100644 index 76b8cce..0000000 --- a/carplace/src/app/(public)/app-privacy-ar/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppPrivacyArPage() { - return -} diff --git a/carplace/src/app/(public)/app-privacy-en/page.tsx b/carplace/src/app/(public)/app-privacy-en/page.tsx deleted file mode 100644 index 202d028..0000000 --- a/carplace/src/app/(public)/app-privacy-en/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppPrivacyEnPage() { - return -} diff --git a/carplace/src/app/(public)/app-privacy-fr/page.tsx b/carplace/src/app/(public)/app-privacy-fr/page.tsx deleted file mode 100644 index 59d917a..0000000 --- a/carplace/src/app/(public)/app-privacy-fr/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppPrivacyFrPage() { - return -} diff --git a/carplace/src/app/(public)/app-tc-ar/page.tsx b/carplace/src/app/(public)/app-tc-ar/page.tsx deleted file mode 100644 index 5f6d7a7..0000000 --- a/carplace/src/app/(public)/app-tc-ar/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppTermsArPage() { - return -} diff --git a/carplace/src/app/(public)/app-tc-en/page.tsx b/carplace/src/app/(public)/app-tc-en/page.tsx deleted file mode 100644 index 9e4f5e4..0000000 --- a/carplace/src/app/(public)/app-tc-en/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppTermsEnPage() { - return -} diff --git a/carplace/src/app/(public)/app-tc-fr/page.tsx b/carplace/src/app/(public)/app-tc-fr/page.tsx deleted file mode 100644 index 54c7443..0000000 --- a/carplace/src/app/(public)/app-tc-fr/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import FooterContentPage from '@/components/FooterContentPage' - -export default function AppTermsFrPage() { - return -} diff --git a/carplace/src/app/(public)/company-workspace/page.tsx b/carplace/src/app/(public)/company-workspace/page.tsx deleted file mode 100644 index 174130b..0000000 --- a/carplace/src/app/(public)/company-workspace/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from 'next/navigation' - -export default function CompanyWorkspacePage() { - redirect('/') -} diff --git a/carplace/src/app/(public)/explore/ExploreSearchForm.tsx b/carplace/src/app/(public)/explore/ExploreSearchForm.tsx deleted file mode 100644 index 408cd02..0000000 --- a/carplace/src/app/(public)/explore/ExploreSearchForm.tsx +++ /dev/null @@ -1,295 +0,0 @@ -'use client' - -import { CalendarDays, Clock3, Info, MapPin, TicketPercent } from 'lucide-react' -import { useRouter } from 'next/navigation' -import { useState } from 'react' - -type ExploreSearchFormDict = { - searchTitle: string - pickupLocation: string - dropoffLocation: string - sameDropoff: string - differentDropoff: string - sameAsPickup: string - pickupDate: string - returnDate: string - hour: string - promoCode: string - myAge: string - ageDescription: string - search: string - ageOptions: string[] -} - -function toUtcDateTime(date: string, time: string) { - if (!date) return '' - return `${date}T${time || '10:00'}:00.000Z` -} - -const inputBase = 'w-full border-0 bg-white dark:bg-blue-900/30 px-4 py-4 text-sm text-stone-900 dark:text-stone-100 outline-none placeholder:text-stone-400 dark:placeholder:text-stone-500' -const selectBase = 'w-full border-0 bg-white dark:bg-blue-900/30 py-4 text-sm text-stone-900 dark:text-stone-100 outline-none' - -export default function ExploreSearchForm({ - pickupLocation, - dropoffLocation, - dropoffMode, - pickupDate, - pickupTime, - returnDate, - returnTime, - promoCode, - driverAge, - cities, - dict, -}: { - pickupLocation: string - dropoffLocation: string - dropoffMode: 'same' | 'different' - pickupDate: string - pickupTime: string - returnDate: string - returnTime: string - promoCode: string - driverAge: string - cities: string[] - dict: ExploreSearchFormDict -}) { - const router = useRouter() - const cityOptions = Array.isArray(cities) ? cities : [] - const ageOptions = Array.isArray(dict.ageOptions) ? dict.ageOptions : [] - const [filters, setFilters] = useState({ - pickupLocation, - dropoffLocation, - dropoffMode, - pickupDate, - pickupTime: pickupTime || '10:00', - returnDate, - returnTime: returnTime || '10:00', - promoCode, - driverAge: driverAge || '25+', - }) - const [ageHelpOpen, setAgeHelpOpen] = useState(false) - - function updateFilter(name: keyof typeof filters, value: string) { - setFilters((current) => ({ ...current, [name]: value })) - } - - function handleSubmit(event: React.FormEvent) { - event.preventDefault() - - const query = new URLSearchParams() - const normalizedPickup = filters.pickupLocation.trim() - const normalizedDropoff = filters.dropoffMode === 'different' - ? filters.dropoffLocation.trim() - : normalizedPickup - const normalizedPromo = filters.promoCode.trim() - - if (normalizedPickup) { - query.set('pickupLocation', normalizedPickup) - query.set('city', normalizedPickup) - } - query.set('dropoffMode', filters.dropoffMode) - if (filters.dropoffMode === 'different' && normalizedDropoff) { - query.set('dropoffLocation', normalizedDropoff) - } - if (filters.pickupDate) query.set('pickupDate', filters.pickupDate) - if (filters.pickupTime) query.set('pickupTime', filters.pickupTime) - if (filters.returnDate) query.set('returnDate', filters.returnDate) - if (filters.returnTime) query.set('returnTime', filters.returnTime) - if (normalizedPromo) query.set('promoCode', normalizedPromo) - if (filters.driverAge) query.set('driverAge', filters.driverAge) - - const startDateTime = toUtcDateTime(filters.pickupDate, filters.pickupTime) - const endDateTime = toUtcDateTime(filters.returnDate, filters.returnTime) - if (startDateTime) query.set('startDate', startDateTime) - if (endDateTime) query.set('endDate', endDateTime) - - const target = query.toString() ? `/explore?${query.toString()}` : '/explore' - router.push(target) - } - - return ( -
-

{dict.searchTitle}

-
-
- -
-
- {dict.dropoffLocation} -
- - -
-
- {filters.dropoffMode === 'different' ? ( - - ) : ( -
- - {filters.pickupLocation || dict.sameAsPickup} -
- )} -
-
- -
- - - - -
- -
- - - -
-
-
- ) -} diff --git a/carplace/src/app/(public)/explore/ExploreVehicleGrid.tsx b/carplace/src/app/(public)/explore/ExploreVehicleGrid.tsx deleted file mode 100644 index 52b8586..0000000 --- a/carplace/src/app/(public)/explore/ExploreVehicleGrid.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import Link from 'next/link' -import { formatCurrency, type Locale } from '@rentaldrivego/types' - -interface Vehicle { - id: string - make: string - model: string - year: number - category: string - dailyRate: number - photos: string[] - availability?: boolean | null - availabilityStatus?: 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE' - nextAvailableAt?: string | null - company: { - slug: string - brand: { - displayName: string - logoUrl: string | null - subdomain: string - marketplaceRating: number | null - } | null - } -} - -interface Dict { - rentalCompany: string - checkAvailability: string - available: string - reserved: string - maintenance: string - unavailableStatus: string - from: string - viewVehicle: string - availableFrom: string - unavailableNoDate: string - vehicleUnavailable: string - listings: string - availableVehicles: string -} - -function formatCents(cents: number, language: Locale) { - return formatCurrency(cents, 'MAD', language) -} - -function formatAvailabilityDate(value?: string | null) { - if (!value) return null - return new Date(value).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' }) -} - -function getStatusCopy(vehicle: Vehicle, dict: Dict) { - if (vehicle.availabilityStatus === 'RESERVED') return dict.reserved - if (vehicle.availabilityStatus === 'MAINTENANCE') return dict.maintenance - if (vehicle.availabilityStatus === 'UNAVAILABLE') return dict.unavailableStatus - if (vehicle.availability === false) return dict.checkAvailability - return dict.available -} - -export default function ExploreVehicleGrid({ vehicles, dict, language }: { vehicles: Vehicle[]; dict: Dict; language: Locale }) { - return ( -
-
-

{dict.availableVehicles}

-

{vehicles.length} {dict.listings}

-
-
- {vehicles.map((vehicle) => ( -
-
- {vehicle.photos[0] ? ( - // eslint-disable-next-line @next/next/no-img-element - {`${vehicle.make} - ) : null} -
-
-
-
-

{vehicle.company.brand?.displayName ?? dict.rentalCompany}

-

{vehicle.make} {vehicle.model}

-

{vehicle.year} · {vehicle.category}

-
- - {getStatusCopy(vehicle, dict)} - -
- {vehicle.availability === false && ( -

- {vehicle.nextAvailableAt ? `${dict.availableFrom} ${formatAvailabilityDate(vehicle.nextAvailableAt)}` : dict.unavailableNoDate} -

- )} -
-
-

{dict.from}

-

{formatCents(vehicle.dailyRate, language)}

-
- - {dict.viewVehicle} - -
-
-
- ))} - {vehicles.length === 0 && ( -
{dict.vehicleUnavailable}
- )} -
-
- ) -} diff --git a/carplace/src/app/(public)/explore/[slug]/page.tsx b/carplace/src/app/(public)/explore/[slug]/page.tsx deleted file mode 100644 index b054926..0000000 --- a/carplace/src/app/(public)/explore/[slug]/page.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import Link from 'next/link' -import { carplaceFetchOrDefault } from '@/lib/api' -import { getCarplaceLanguage } from '@/lib/i18n.server' -import { formatCurrency } from '@rentaldrivego/types' - -interface CompanyProfile { - id: string - name: string - slug: string - brand: { - displayName: string - logoUrl: string | null - publicCity: string | null - publicCountry: string | null - publicPhone: string | null - whatsappNumber: string | null - marketplaceRating: number | null - } | null - vehicles: Array<{ id: string; make: string; model: string; dailyRate: number; photos: string[]; category: string; availability: boolean; nextAvailableAt: string | null }> - offers: Array<{ id: string; title: string; discountValue: number; validUntil: string }> -} - -export default async function CompanyProfilePage({ params }: { params: { slug: string } }) { - const language = await getCarplaceLanguage() - const dict = { - en: { - unavailable: 'Carplace unavailable', - unavailableTitle: 'Company details are temporarily unavailable.', - unavailableBody: 'The carplace API is running, but the database is not reachable in this local environment.', - backToExplore: 'Back to explore', - partner: 'Carplace partner', - vehicles: 'vehicles', - offers: 'active offers', - rating: 'Rating', - new: 'New', - currentOffers: 'Current offers', - validUntil: 'Valid until', - noOffers: 'No active offers right now.', - fleet: 'Fleet', - viewVehicle: 'View vehicle', - availableFrom: 'Available from', - unavailableNoDate: 'Temporarily unavailable for new reservations.', - }, - fr: { - unavailable: 'Carplace indisponible', - unavailableTitle: "Les détails de l'entreprise sont temporairement indisponibles.", - unavailableBody: "L'API carplace fonctionne, mais la base de données n'est pas accessible dans cet environnement local.", - backToExplore: "Retour à l'exploration", - partner: 'Partenaire carplace', - vehicles: 'véhicules', - offers: 'offres actives', - rating: 'Note', - new: 'Nouveau', - currentOffers: 'Offres actuelles', - validUntil: "Valable jusqu'au", - noOffers: 'Aucune offre active pour le moment.', - fleet: 'Flotte', - viewVehicle: 'Voir le véhicule', - availableFrom: 'Disponible à partir du', - unavailableNoDate: 'Temporairement indisponible pour les nouvelles réservations.', - }, - ar: { - unavailable: 'السوق غير متاح', - unavailableTitle: 'تفاصيل الشركة غير متاحة مؤقتاً.', - unavailableBody: 'واجهة السوق تعمل، لكن قاعدة البيانات غير متاحة في هذه البيئة المحلية.', - backToExplore: 'العودة إلى الاستكشاف', - partner: 'شريك في السوق', - vehicles: 'مركبات', - offers: 'عروض نشطة', - rating: 'التقييم', - new: 'جديد', - currentOffers: 'العروض الحالية', - validUntil: 'صالح حتى', - noOffers: 'لا توجد عروض نشطة حالياً.', - fleet: 'الأسطول', - viewVehicle: 'عرض السيارة', - availableFrom: 'متاح ابتداءً من', - unavailableNoDate: 'غير متاحة مؤقتاً للحجوزات الجديدة.', - }, - }[language] - const company = await carplaceFetchOrDefault(`/carplace/${params.slug}`, null) - - if (!company) { - return ( -
-
-
-

{dict.unavailable}

-

{dict.unavailableTitle}

-

{dict.unavailableBody}

-
- {dict.backToExplore} -
-
-
-
- ) - } - - return ( -
-
-
-

{company.brand?.publicCity ?? dict.partner}

-

{company.brand?.displayName ?? company.name}

-

{company.brand?.publicCountry ?? ''}

-
- {company.vehicles.length} {dict.vehicles} - {company.offers.length} {dict.offers} - {dict.rating} {company.brand?.marketplaceRating?.toFixed(1) ?? dict.new} -
-
- -
-

{dict.currentOffers}

-
- {company.offers.map((offer) => ( -
-

{offer.title}

-

{offer.discountValue}%

-

{dict.validUntil} {new Date(offer.validUntil).toLocaleDateString()}

-
- ))} - {company.offers.length === 0 &&
{dict.noOffers}
} -
-
- -
-

{dict.fleet}

-
- {company.vehicles.map((vehicle) => ( -
-
- {vehicle.photos[0] ? ( - // eslint-disable-next-line @next/next/no-img-element - {`${vehicle.make} - ) : null} -
-
-

{vehicle.make} {vehicle.model}

-

{vehicle.category}

- {!vehicle.availability && ( -

- {vehicle.nextAvailableAt ? `${dict.availableFrom} ${new Date(vehicle.nextAvailableAt).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}` : dict.unavailableNoDate} -

- )} -
-

{formatCurrency(vehicle.dailyRate, 'MAD', language)}

- {dict.viewVehicle} -
-
-
- ))} -
-
-
-
- ) -} diff --git a/carplace/src/app/(public)/explore/[slug]/vehicles/[id]/page.tsx b/carplace/src/app/(public)/explore/[slug]/vehicles/[id]/page.tsx deleted file mode 100644 index b9c6d3e..0000000 --- a/carplace/src/app/(public)/explore/[slug]/vehicles/[id]/page.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import Link from 'next/link' -import { carplaceFetchOrDefault } from '@/lib/api' -import { getCarplaceLanguage } from '@/lib/i18n.server' -import { formatCurrency } from '@rentaldrivego/types' -import BookingForm from '@/components/BookingForm' - -interface VehicleDetail { - id: string - make: string - model: string - year: number - category: string - seats: number - transmission: string - fuelType: string - features: string[] - dailyRate: number - photos: string[] - pickupLocations: string[] - allowDifferentDropoff: boolean - dropoffLocations: string[] - availability: boolean - availabilityStatus: 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE' - nextAvailableAt: string | null - company: { - brand: { - displayName: string - subdomain: string - publicPhone: string | null - whatsappNumber: string | null - } | null - } -} - -export default async function VehicleDetailPage({ params }: { params: { slug: string; id: string } }) { - const language = await getCarplaceLanguage() - const dict = { - en: { - unavailable: 'Carplace unavailable', - unavailableTitle: 'Vehicle details are temporarily unavailable.', - unavailableBody: 'The carplace frontend is running, but the backing database is not reachable right now.', - backToFleet: 'Back to fleet', - backToExplore: 'Back to explore', - noPhotos: 'No photos available.', - rentalCompany: 'Rental company', - perDay: '/ day', - availableFrom: 'Available from', - unavailableNoDate: 'This vehicle is temporarily unavailable for new reservations.', - specs: 'Specifications', - year: 'Year', - category: 'Category', - seats: 'Seats', - transmission: 'Transmission', - fuel: 'Fuel type', - features: 'Features & extras', - contact: 'Contact company', - phone: 'Phone', - whatsapp: 'WhatsApp', - photos: 'Photos', - }, - fr: { - unavailable: 'Carplace indisponible', - unavailableTitle: 'Les détails du véhicule sont temporairement indisponibles.', - unavailableBody: "Le frontend carplace fonctionne, mais la base de données n'est pas accessible pour le moment.", - backToFleet: 'Retour à la flotte', - backToExplore: 'Retour à l’exploration', - noPhotos: 'Aucune photo disponible.', - rentalCompany: 'Société de location', - perDay: '/ jour', - availableFrom: 'Disponible à partir du', - unavailableNoDate: 'Ce véhicule est temporairement indisponible pour les nouvelles réservations.', - specs: 'Caractéristiques', - year: 'Année', - category: 'Catégorie', - seats: 'Places', - transmission: 'Transmission', - fuel: 'Carburant', - features: 'Équipements & extras', - contact: 'Contacter la société', - phone: 'Téléphone', - whatsapp: 'WhatsApp', - photos: 'Photos', - }, - ar: { - unavailable: 'السوق غير متاح', - unavailableTitle: 'تفاصيل السيارة غير متاحة مؤقتاً.', - unavailableBody: 'واجهة السوق تعمل، لكن قاعدة البيانات في الخلفية غير متاحة حالياً.', - backToFleet: 'العودة إلى الأسطول', - backToExplore: 'العودة إلى الاستكشاف', - noPhotos: 'لا توجد صور متاحة.', - rentalCompany: 'شركة تأجير', - perDay: '/ يوم', - availableFrom: 'متاح ابتداءً من', - unavailableNoDate: 'هذه السيارة غير متاحة مؤقتاً للحجوزات الجديدة.', - specs: 'المواصفات', - year: 'السنة', - category: 'الفئة', - seats: 'المقاعد', - transmission: 'ناقل الحركة', - fuel: 'نوع الوقود', - features: 'المميزات والإضافات', - contact: 'تواصل مع الشركة', - phone: 'الهاتف', - whatsapp: 'واتساب', - photos: 'الصور', - }, - }[language] - - const vehicle = await carplaceFetchOrDefault(`/carplace/${params.slug}/vehicles/${params.id}`, null) - - if (!vehicle) { - return ( -
-
-
-

{dict.unavailable}

-

{dict.unavailableTitle}

-

{dict.unavailableBody}

-
- {dict.backToFleet} - {dict.backToExplore} -
-
-
-
- ) - } - - const availabilityDate = vehicle.nextAvailableAt - ? new Date(vehicle.nextAvailableAt).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' }) - : null - - const [heroPhoto, ...galleryPhotos] = vehicle.photos - - return ( -
-
- - {/* Breadcrumb */} - - -
- - {/* Left — photos + specs */} -
- - {/* Hero photo */} - {heroPhoto ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${vehicle.make} -
- ) : ( -
{dict.noPhotos}
- )} - - {/* Gallery */} - {galleryPhotos.length > 0 && ( -
-

{dict.photos}

-
- {galleryPhotos.map((photo, index) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${vehicle.make} -
- ))} -
-
- )} - - {/* Specs */} -
-

{dict.specs}

-
-
-
{dict.year}
-
{vehicle.year}
-
-
-
{dict.category}
-
{vehicle.category}
-
- {vehicle.seats > 0 && ( -
-
{dict.seats}
-
{vehicle.seats}
-
- )} - {vehicle.transmission && ( -
-
{dict.transmission}
-
{vehicle.transmission}
-
- )} - {vehicle.fuelType && ( -
-
{dict.fuel}
-
{vehicle.fuelType}
-
- )} -
- - {vehicle.features.length > 0 && ( -
-

{dict.features}

-
- {vehicle.features.map((feature) => ( - {feature} - ))} -
-
- )} -
- - {/* Contact */} - {(vehicle.company.brand?.publicPhone || vehicle.company.brand?.whatsappNumber) && ( -
-

{dict.contact}

-

{vehicle.company.brand?.displayName ?? dict.rentalCompany}

-
- {vehicle.company.brand?.publicPhone && ( - - {dict.phone}: {vehicle.company.brand.publicPhone} - - )} - {vehicle.company.brand?.whatsappNumber && ( - - {dict.whatsapp} - - )} -
-
- )} -
- - {/* Right — sticky booking panel */} - -
-
-
- ) -} diff --git a/carplace/src/app/(public)/explore/page.tsx b/carplace/src/app/(public)/explore/page.tsx deleted file mode 100644 index 8a2113e..0000000 --- a/carplace/src/app/(public)/explore/page.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import Link from 'next/link' -import { carplaceFetchOrDefault } from '@/lib/api' -import { getCarplaceLanguage } from '@/lib/i18n.server' -import ExploreSearchForm from './ExploreSearchForm' -import ExploreVehicleGrid from './ExploreVehicleGrid' - -interface Vehicle { - id: string - make: string - model: string - year: number - category: string - dailyRate: number - photos: string[] - pickupLocations: string[] - allowDifferentDropoff: boolean - dropoffLocations: string[] - availability?: boolean | null - availabilityStatus?: 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE' - nextAvailableAt?: string | null - company: { - slug: string - brand: { - displayName: string - logoUrl: string | null - subdomain: string - marketplaceRating: number | null - } | null - } -} - -interface Offer { - id: string - title: string - discountValue: number - validUntil: string - company: { - brand: { - displayName: string - subdomain: string - } | null - } -} - -interface CompanyCard { - id: string - brand: { - displayName: string - subdomain: string - publicCity: string | null - marketplaceRating: number | null - } | null - _count: { - vehicles: number - } -} - -const CATEGORY_VALUES = [ - 'ECONOMY', - 'COMPACT', - 'MIDSIZE', - 'FULLSIZE', - 'SUV', - 'LUXURY', - 'VAN', - 'TRUCK', -] as const - -export default async function ExplorePage({ searchParams }: { searchParams?: Promise> }) { - const resolvedSearchParams = await searchParams - const language = await getCarplaceLanguage() - const dict = { - en: { - kicker: 'Discovery only', - title: 'Find your next rental from trusted local companies.', - body: 'Browse, filter, and reserve — the company confirms and contacts you directly.', - searchTitle: 'Find a vehicle', - pickupLocation: 'Pick-up location', - dropoffLocation: 'Drop-off location', - sameDropoff: 'Same drop-off', - differentDropoff: 'Different drop-off', - sameAsPickup: 'Return to the same location', - pickupDate: 'Start date', - returnDate: 'Return date', - hour: 'Hour', - promoCode: 'I have a promo code', - myAge: 'My age', - ageDescription: 'Driver age can affect availability, deposit amount, and final pricing.', - ageOptions: ['18+', '21+', '23+', '25+', '30+'], - search: 'Search', - currentDeals: 'Current deals', - featuredOffers: 'Featured carplace offers', - company: 'Company', - validUntil: 'Valid until', - offersUnavailable: 'Carplace offers are unavailable right now.', - availableVehicles: 'Available vehicles', - listings: 'listings', - rentalCompany: 'Rental company', - checkAvailability: 'Check availability', - available: 'Available', - reserved: 'Reserved', - maintenance: 'In maintenance', - unavailableStatus: 'Unavailable', - from: 'From', - viewVehicle: 'View vehicle', - availableFrom: 'Available from', - unavailableNoDate: 'This vehicle is temporarily unavailable for new reservations.', - vehicleUnavailable: 'Vehicle listings are unavailable right now.', - browseByCategory: 'Browse by category', - browseByCompany: 'Browse by company', - carplacePartners: 'Carplace partners', - carplacePartner: 'Carplace partner', - rating: 'Rating', - new: 'New', - publishedVehicles: 'published vehicles', - viewFleet: 'View fleet', - categories: ['Economy', 'Compact', 'Midsize', 'Full-size', 'SUV', 'Luxury', 'Van', 'Truck'], - }, - fr: { - kicker: 'Découverte uniquement', - title: "Trouvez votre prochaine location auprès d'entreprises locales fiables.", - body: "Parcourez, filtrez et réservez — l'entreprise confirme et vous contacte directement.", - searchTitle: 'Trouver un véhicule', - pickupLocation: 'Lieu de départ', - dropoffLocation: "Lieu de retour", - sameDropoff: 'Même retour', - differentDropoff: 'Retour différent', - sameAsPickup: 'Retour au même lieu', - pickupDate: 'Date de départ', - returnDate: 'Date de retour', - hour: 'Heure', - promoCode: "J'ai un code promo", - myAge: 'Mon âge', - ageDescription: "L'âge du conducteur peut influencer la disponibilité, le dépôt de garantie et le prix final.", - ageOptions: ['18+', '21+', '23+', '25+', '30+'], - search: 'Rechercher', - currentDeals: 'Offres du moment', - featuredOffers: 'Offres carplace mises en avant', - company: 'Entreprise', - validUntil: "Valable jusqu'au", - offersUnavailable: 'Les offres carplace sont indisponibles pour le moment.', - availableVehicles: 'Véhicules disponibles', - listings: 'annonces', - rentalCompany: 'Société de location', - checkAvailability: 'Vérifier la disponibilité', - available: 'Disponible', - reserved: 'Réservé', - maintenance: 'En maintenance', - unavailableStatus: 'Indisponible', - from: 'À partir de', - viewVehicle: 'Voir le véhicule', - availableFrom: 'Disponible à partir du', - unavailableNoDate: 'Ce véhicule est temporairement indisponible pour les nouvelles réservations.', - vehicleUnavailable: 'Les annonces véhicules sont indisponibles pour le moment.', - browseByCategory: 'Parcourir par catégorie', - browseByCompany: 'Parcourir par entreprise', - carplacePartners: 'Partenaires carplace', - carplacePartner: 'Partenaire carplace', - rating: 'Note', - new: 'Nouveau', - publishedVehicles: 'véhicules publiés', - viewFleet: 'Voir la flotte', - categories: ['Économie', 'Compacte', 'Intermédiaire', 'Grande berline', 'SUV', 'Luxe', 'Van', 'Camion'], - }, - ar: { - kicker: 'للاستكشاف فقط', - title: 'اعثر على سيارتك القادمة من شركات محلية موثوقة.', - body: 'تصفّح وابحث واحجز — تؤكد الشركة وتتواصل معك مباشرةً.', - searchTitle: 'احجز سيارة', - pickupLocation: 'موقع الاستلام', - dropoffLocation: 'موقع الإرجاع', - sameDropoff: 'نفس موقع الإرجاع', - differentDropoff: 'موقع إرجاع مختلف', - sameAsPickup: 'الإرجاع في نفس موقع الاستلام', - pickupDate: 'تاريخ البدء', - returnDate: 'تاريخ الإرجاع', - hour: 'الساعة', - promoCode: 'لدي رمز ترويجي', - myAge: 'عمري', - ageDescription: 'قد يؤثر عمر السائق على التوفر ومبلغ التأمين والسعر النهائي.', - ageOptions: ['18+', '21+', '23+', '25+', '30+'], - search: 'بحث', - currentDeals: 'العروض الحالية', - featuredOffers: 'عروض السوق المميزة', - company: 'شركة', - validUntil: 'صالح حتى', - offersUnavailable: 'عروض السوق غير متاحة حالياً.', - availableVehicles: 'السيارات المتاحة', - listings: 'إعلانات', - rentalCompany: 'شركة تأجير', - checkAvailability: 'تحقق من التوفر', - available: 'متاح', - reserved: 'محجوز', - maintenance: 'قيد الصيانة', - unavailableStatus: 'غير متاح', - from: 'ابتداءً من', - viewVehicle: 'عرض السيارة', - availableFrom: 'متاح ابتداءً من', - unavailableNoDate: 'هذه السيارة غير متاحة مؤقتاً للحجوزات الجديدة.', - vehicleUnavailable: 'قوائم السيارات غير متاحة حالياً.', - browseByCategory: 'تصفح حسب الفئة', - browseByCompany: 'تصفح حسب الشركة', - carplacePartners: 'شركاء السوق', - carplacePartner: 'شريك في السوق', - rating: 'التقييم', - new: 'جديد', - publishedVehicles: 'سيارات منشورة', - viewFleet: 'عرض الأسطول', - categories: ['اقتصادية', 'مدمجة', 'متوسطة', 'كبيرة', 'SUV', 'فاخرة', 'فان', 'شاحنة'], - }, - }[language] - - const city = typeof resolvedSearchParams?.city === 'string' ? resolvedSearchParams.city : '' - const pickupLocation = typeof resolvedSearchParams?.pickupLocation === 'string' ? resolvedSearchParams.pickupLocation : city - const dropoffLocation = typeof resolvedSearchParams?.dropoffLocation === 'string' ? resolvedSearchParams.dropoffLocation : '' - const dropoffMode = resolvedSearchParams?.dropoffMode === 'different' ? 'different' : 'same' - const pickupDate = typeof resolvedSearchParams?.pickupDate === 'string' ? resolvedSearchParams.pickupDate : '' - const pickupTime = typeof resolvedSearchParams?.pickupTime === 'string' ? resolvedSearchParams.pickupTime : '10:00' - const returnDate = typeof resolvedSearchParams?.returnDate === 'string' ? resolvedSearchParams.returnDate : '' - const returnTime = typeof resolvedSearchParams?.returnTime === 'string' ? resolvedSearchParams.returnTime : '10:00' - const promoCode = typeof resolvedSearchParams?.promoCode === 'string' ? resolvedSearchParams.promoCode : '' - const driverAge = typeof resolvedSearchParams?.driverAge === 'string' ? resolvedSearchParams.driverAge : '23+' - const category = typeof resolvedSearchParams?.category === 'string' ? resolvedSearchParams.category : '' - const transmission = typeof resolvedSearchParams?.transmission === 'string' ? resolvedSearchParams.transmission : '' - const make = typeof resolvedSearchParams?.make === 'string' ? resolvedSearchParams.make : '' - const model = typeof resolvedSearchParams?.model === 'string' ? resolvedSearchParams.model : '' - const startDate = typeof resolvedSearchParams?.startDate === 'string' ? resolvedSearchParams.startDate : '' - const endDate = typeof resolvedSearchParams?.endDate === 'string' ? resolvedSearchParams.endDate : '' - - const query = new URLSearchParams() - if (pickupLocation) query.set('city', pickupLocation) - if (pickupLocation) query.set('pickupLocation', pickupLocation) - query.set('dropoffMode', dropoffMode) - if (dropoffMode === 'different' && dropoffLocation) query.set('dropoffLocation', dropoffLocation) - if (startDate) query.set('startDate', startDate) - if (endDate) query.set('endDate', endDate) - if (category) query.set('category', category) - if (transmission) query.set('transmission', transmission) - if (make) query.set('make', make) - if (model) query.set('model', model) - query.set('pageSize', '24') - - const [offers, vehicles, companies] = await Promise.all([ - carplaceFetchOrDefault('/carplace/offers', []), - carplaceFetchOrDefault(`/carplace/search?${query.toString()}`, []), - carplaceFetchOrDefault('/carplace/companies?pageSize=8', []), - ]) - const cities = Array.from( - new Set( - companies - .map((company) => company.brand?.publicCity?.trim()) - .filter((city): city is string => Boolean(city)) - ) - ).sort((left, right) => left.localeCompare(right)) - - return ( -
-
-
-

{dict.kicker}

-

{dict.title}

-

{dict.body}

- -
- -
-
-

{dict.currentDeals}

-

{dict.featuredOffers}

-
-
- {offers.map((offer) => ( - -

{offer.company.brand?.displayName ?? dict.company}

-

{offer.title}

-

{offer.discountValue}%

-

{dict.validUntil} {new Date(offer.validUntil).toLocaleDateString()}

- - ))} - {offers.length === 0 &&
{dict.offersUnavailable}
} -
-
- - - -
-

{dict.browseByCategory}

-
- {dict.categories.map((categoryName, index) => ( - - {categoryName} - - ))} -
-
- -
-
-

{dict.browseByCompany}

-

{dict.carplacePartners}

-
-
- {companies.map((company) => ( - -

{company.brand?.publicCity ?? dict.carplacePartner}

-

{company.brand?.displayName ?? dict.rentalCompany}

-

{dict.rating} {company.brand?.marketplaceRating?.toFixed(1) ?? dict.new}

-

{company._count.vehicles} {dict.publishedVehicles}

- {dict.viewFleet} - - ))} -
-
-
-
- ) -} diff --git a/carplace/src/app/(public)/footer/[slug]/page.test.ts b/carplace/src/app/(public)/footer/[slug]/page.test.ts deleted file mode 100644 index 8e930d9..0000000 --- a/carplace/src/app/(public)/footer/[slug]/page.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import React, { isValidElement } from 'react' -import { describe, expect, it, vi } from 'vitest' - -const navigation = vi.hoisted(() => ({ - notFound: vi.fn(() => { - throw new Error('NEXT_NOT_FOUND') - }), -})) - -vi.mock('next/navigation', () => navigation) - -import FooterContentPage from '@/components/FooterContentPage' -import FooterPage, { generateStaticParams } from './page' -import { footerPageSlugs } from '@/lib/footerContent' - -describe('carplace footer route', () => { - it('generates one static route per registered footer slug', () => { - expect(generateStaticParams()).toEqual(footerPageSlugs.map((slug) => ({ slug }))) - }) - - it('passes valid slugs through to FooterContentPage', () => { - const element = FooterPage({ params: { slug: 'privacy-policy' } }) - - expect(isValidElement(element)).toBe(true) - expect(element.type).toBe(FooterContentPage) - expect(element.props.slug).toBe('privacy-policy') - expect(element.props.forcedLanguage).toBeUndefined() - }) - - it('rejects unknown slugs instead of rendering arbitrary policy pages', () => { - expect(() => FooterPage({ params: { slug: 'definitely-not-a-policy' } })).toThrow('NEXT_NOT_FOUND') - expect(navigation.notFound).toHaveBeenCalled() - }) -}) diff --git a/carplace/src/app/(public)/footer/[slug]/page.tsx b/carplace/src/app/(public)/footer/[slug]/page.tsx deleted file mode 100644 index f9ddf5a..0000000 --- a/carplace/src/app/(public)/footer/[slug]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { notFound } from 'next/navigation' -import FooterContentPage from '@/components/FooterContentPage' -import { footerPageSlugs, isFooterPageSlug } from '@/lib/footerContent' - -export function generateStaticParams() { - return footerPageSlugs.map((slug) => ({ slug })) -} - -export default function FooterPage({ - params, -}: { - params: { slug: string } -}) { - const { slug } = params - - if (!isFooterPageSlug(slug)) { - notFound() - } - - return -} diff --git a/carplace/src/app/(public)/layout.tsx b/carplace/src/app/(public)/layout.tsx deleted file mode 100644 index 5bbcb07..0000000 --- a/carplace/src/app/(public)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type { ReactNode } from 'react' -import SitePageLayout from '@/components/public/SitePageLayout' - -export default function PublicLayout({ children }: { children: ReactNode }) { - return {children} -} diff --git a/carplace/src/app/(public)/page.tsx b/carplace/src/app/(public)/page.tsx deleted file mode 100644 index 253cdde..0000000 --- a/carplace/src/app/(public)/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from 'next/navigation' - -export default function CarplaceHomePage() { - redirect('/explore') -} diff --git a/carplace/src/app/globals.css b/carplace/src/app/globals.css deleted file mode 100644 index ef5ae67..0000000 --- a/carplace/src/app/globals.css +++ /dev/null @@ -1,293 +0,0 @@ -@import "../styles/phase16-tokens.css"; - -@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, #0a1535 0%, #0d1f52 35%, #091228 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 60 140 / 0.40); - background-color: rgb(11 25 55 / 0.72); -} - -.site-page { - @apply min-h-screen overflow-hidden text-blue-950; - background-image: - linear-gradient(180deg, #ffffff 0%, #f5f8ff 28%, #eef4ff 58%, #ffffff 100%); -} - -html.dark .site-page { - @apply text-slate-100; - background-image: - linear-gradient(180deg, #0a1535 0%, #0d1f52 35%, #091228 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(96, 165, 250, 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 60 140 / 0.40); - background-color: rgb(11 25 55 / 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, #f5f8ff 0%, #edf2ff 100%); -} - -html.dark .site-panel-muted { - border-color: rgb(30 60 140 / 0.40); - background-image: linear-gradient(160deg, #0c1830 0%, #10203e 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(14 40 90 / 0.8); - background-color: rgb(10 25 75); -} - -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-blue-800 dark:bg-blue-950/30 dark:text-blue-100 dark:hover:border-blue-400 dark:hover:text-white; -} - -/* Phase 16 operational application layer */ -@layer base { - html { - min-width: 320px; - background: var(--rdg-surface-page); - color: var(--rdg-text-primary); - scroll-behavior: smooth; - text-rendering: optimizeLegibility; - } - - body { - min-height: 100dvh; - background: var(--rdg-surface-page); - color: var(--rdg-text-primary); - font-family: var(--rdg-font-latin); - line-height: 1.6; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - color: var(--rdg-text-primary); - line-height: 1.18; - text-wrap: balance; - } - - p { - text-wrap: pretty; - } - - button, - input, - select, - textarea { - font: inherit; - } - - a, - button, - input, - select, - textarea { - -webkit-tap-highlight-color: transparent; - } -} - -@layer components { - .shell { - width: min(100%, var(--rdg-container-max)); - } - - .panel, - .card, - .glass-card, - .site-panel { - border-color: var(--rdg-border); - border-radius: var(--rdg-radius-lg); - background: color-mix(in srgb, var(--rdg-surface-elevated) 92%, transparent); - box-shadow: var(--rdg-shadow-card); - backdrop-filter: blur(18px); - -webkit-backdrop-filter: blur(18px); - } - - .site-panel-muted { - border-color: var(--rdg-border); - border-radius: var(--rdg-radius-lg); - background: var(--rdg-surface-muted); - } - - .site-panel-contrast { - border-color: color-mix(in srgb, var(--rdg-blue-700) 68%, var(--rdg-border)); - border-radius: var(--rdg-radius-lg); - background: - radial-gradient(circle at 90% 10%, color-mix(in srgb, var(--rdg-blue-500) 30%, transparent), transparent 40%), - linear-gradient(135deg, var(--rdg-navy-900), var(--rdg-blue-800)); - } - - .btn-primary, - .site-link-primary { - min-height: var(--rdg-touch-min); - border-radius: var(--rdg-radius-sm); - background: var(--rdg-action-conversion); - color: #ffffff; - box-shadow: var(--rdg-shadow-subtle); - } - - .btn-primary:hover, - .site-link-primary:hover { - background: var(--rdg-action-conversion-hover); - } - - .btn-secondary, - .site-link-secondary { - min-height: var(--rdg-touch-min); - border-color: var(--rdg-border-strong); - border-radius: var(--rdg-radius-sm); - background: var(--rdg-surface-primary); - color: var(--rdg-action-primary); - } - - .btn-secondary:hover, - .site-link-secondary:hover { - border-color: var(--rdg-action-primary); - background: var(--rdg-soft-blue); - color: var(--rdg-action-primary-hover); - } - - .input-field, - input:not([type='checkbox']):not([type='radio']):not([type='range']), - select, - textarea { - min-height: var(--rdg-touch-min); - border-color: var(--rdg-border); - border-radius: var(--rdg-radius-sm); - background: var(--rdg-surface-primary); - color: var(--rdg-text-primary); - } - - .input-field::placeholder, - input::placeholder, - textarea::placeholder { - color: var(--rdg-text-subdued); - } - - table { - border-color: var(--rdg-border); - } - - thead { - background: var(--rdg-surface-muted); - color: var(--rdg-text-secondary); - } - - tbody tr { - border-color: var(--rdg-border); - } -} - -@media (forced-colors: active) { - .panel, - .card, - .glass-card, - .site-panel, - .site-panel-muted, - .site-panel-contrast { - border: 1px solid CanvasText; - box-shadow: none; - } -} - -html.dark body, -html[data-theme='dark'] body, -.site-page, -html.dark .site-page, -html[data-theme='dark'] .site-page { - background: - radial-gradient(circle at 12% 0%, color-mix(in srgb, var(--rdg-soft-blue) 78%, transparent), transparent 30%), - var(--rdg-surface-page); - color: var(--rdg-text-primary); -} - -.site-glow, -html.dark .site-glow, -html[data-theme='dark'] .site-glow { - background-image: - radial-gradient(circle at top left, color-mix(in srgb, var(--rdg-soft-orange) 72%, transparent), transparent 34%), - radial-gradient(circle at top right, color-mix(in srgb, var(--rdg-soft-blue) 72%, transparent), transparent 28%); -} - -html.dark .card, -html.dark .site-panel, -html.dark .site-panel-muted, -html[data-theme='dark'] .card, -html[data-theme='dark'] .site-panel, -html[data-theme='dark'] .site-panel-muted { - border-color: var(--rdg-border); - background: color-mix(in srgb, var(--rdg-surface-elevated) 92%, transparent); - box-shadow: var(--rdg-shadow-card); -} diff --git a/carplace/src/app/layout.tsx b/carplace/src/app/layout.tsx deleted file mode 100644 index 308bd13..0000000 --- a/carplace/src/app/layout.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { Metadata } from 'next' -import { cookies } from 'next/headers' -import CarplaceShell from '@/components/CarplaceShell' -import { getCarplaceLanguage } from '@/lib/i18n.server' -import './globals.css' - -export const metadata: Metadata = { - title: 'RentalDriveGo Carplace', - description: 'Discover vehicles from trusted rental companies.', - 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 ?? - cookieStore.get('carplace-theme')?.value - const theme = rawTheme === 'dark' ? 'dark' : 'light' - - return ( - - - {/* Runs before hydration to prevent flash of wrong theme */} -