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 */} +