redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s

This commit is contained in:
root
2026-06-26 16:27:21 -04:00
parent 256ff0814e
commit d7fb7b7a7b
1030 changed files with 107374 additions and 2657 deletions
@@ -0,0 +1,295 @@
'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<HTMLFormElement>) {
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 (
<div className="mt-8 rounded-[1.75rem] bg-white dark:bg-blue-950 p-5 text-stone-900 dark:text-stone-100 shadow-[0_24px_80px_rgba(0,0,0,0.16)]">
<h2 className="text-3xl font-black tracking-tight text-stone-900 dark:text-stone-100">{dict.searchTitle}</h2>
<form onSubmit={handleSubmit} className="mt-5 space-y-4">
<div className="grid gap-px overflow-hidden rounded-[1.35rem] border border-stone-200 dark:border-blue-800 bg-stone-200 dark:bg-stone-700 md:grid-cols-2">
<label className="flex items-center gap-3 bg-white dark:bg-blue-900/30 px-4">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<select
name="pickupLocation"
value={filters.pickupLocation}
onChange={(event) => updateFilter('pickupLocation', event.target.value)}
className={selectBase}
>
<option value="">{dict.pickupLocation}</option>
{cityOptions.map((city) => (
<option key={`pickup-${city}`} value={city}>{city}</option>
))}
</select>
</label>
<div className="bg-white dark:bg-blue-900/30 px-4 py-3">
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.dropoffLocation}</span>
<div className="inline-flex rounded-full border border-stone-200 dark:border-blue-800 bg-stone-100 dark:bg-blue-950 p-1">
<button
type="button"
onClick={() => setFilters((current) => ({ ...current, dropoffMode: 'same', dropoffLocation: '' }))}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
filters.dropoffMode === 'same'
? 'bg-blue-900 text-white dark:bg-orange-400 dark:text-white'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{dict.sameDropoff}
</button>
<button
type="button"
onClick={() => setFilters((current) => ({ ...current, dropoffMode: 'different' }))}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
filters.dropoffMode === 'different'
? 'bg-blue-900 text-white dark:bg-orange-400 dark:text-white'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{dict.differentDropoff}
</button>
</div>
</div>
{filters.dropoffMode === 'different' ? (
<label className="mt-3 flex items-center gap-3">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<select
name="dropoffLocation"
value={filters.dropoffLocation}
onChange={(event) => updateFilter('dropoffLocation', event.target.value)}
className={selectBase}
>
<option value="">{dict.dropoffLocation}</option>
{cityOptions.map((city) => (
<option key={`dropoff-${city}`} value={city}>{city}</option>
))}
</select>
</label>
) : (
<div className="mt-3 flex items-center gap-3 rounded-2xl bg-stone-50 px-4 py-4 text-sm font-medium text-stone-500 dark:bg-blue-950 dark:text-stone-300">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<span>{filters.pickupLocation || dict.sameAsPickup}</span>
</div>
)}
</div>
</div>
<div className="grid gap-px overflow-hidden rounded-[1.35rem] border border-stone-200 dark:border-blue-800 bg-stone-200 dark:bg-stone-700 md:grid-cols-4">
<label className="flex items-center gap-3 bg-white dark:bg-blue-900/30 px-4">
<CalendarDays className="h-5 w-5 text-stone-500 dark:text-stone-400" />
<div className="flex w-full flex-col py-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.pickupDate}</span>
<input
name="pickupDate"
type="date"
value={filters.pickupDate}
min={new Date().toISOString().slice(0, 10)}
onChange={(event) => updateFilter('pickupDate', event.target.value)}
className="border-0 bg-transparent px-0 py-0 text-sm text-stone-900 dark:text-stone-100 outline-none [color-scheme:light] dark:[color-scheme:dark]"
/>
</div>
</label>
<label className="flex items-center gap-3 bg-white dark:bg-blue-900/30 px-4">
<Clock3 className="h-5 w-5 text-stone-500 dark:text-stone-400" />
<div className="flex w-full flex-col py-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.hour}</span>
<input
name="pickupTime"
type="time"
value={filters.pickupTime}
onChange={(event) => updateFilter('pickupTime', event.target.value)}
className="border-0 bg-transparent px-0 py-0 text-sm text-stone-900 dark:text-stone-100 outline-none [color-scheme:light] dark:[color-scheme:dark]"
/>
</div>
</label>
<label className="flex items-center gap-3 bg-white dark:bg-blue-900/30 px-4">
<CalendarDays className="h-5 w-5 text-stone-500 dark:text-stone-400" />
<div className="flex w-full flex-col py-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.returnDate}</span>
<input
name="returnDate"
type="date"
value={filters.returnDate}
min={filters.pickupDate || new Date().toISOString().slice(0, 10)}
onChange={(event) => updateFilter('returnDate', event.target.value)}
className="border-0 bg-transparent px-0 py-0 text-sm text-stone-900 dark:text-stone-100 outline-none [color-scheme:light] dark:[color-scheme:dark]"
/>
</div>
</label>
<label className="flex items-center gap-3 bg-white dark:bg-blue-900/30 px-4">
<Clock3 className="h-5 w-5 text-stone-500 dark:text-stone-400" />
<div className="flex w-full flex-col py-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.hour}</span>
<input
name="returnTime"
type="time"
value={filters.returnTime}
onChange={(event) => updateFilter('returnTime', event.target.value)}
className="border-0 bg-transparent px-0 py-0 text-sm text-stone-900 dark:text-stone-100 outline-none [color-scheme:light] dark:[color-scheme:dark]"
/>
</div>
</label>
</div>
<div className="relative z-10 grid gap-px overflow-visible rounded-[1.35rem] border border-stone-200 dark:border-blue-800 bg-stone-200 dark:bg-stone-700 lg:grid-cols-[1.1fr_1.1fr_1.35fr]">
<label className="flex items-center gap-3 rounded-t-[1.35rem] bg-white dark:bg-blue-900/30 px-4 lg:rounded-l-[1.35rem] lg:rounded-tr-none">
<TicketPercent className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<input
name="promoCode"
value={filters.promoCode}
onChange={(event) => updateFilter('promoCode', event.target.value)}
placeholder={dict.promoCode}
className={`${inputBase} pl-2`}
/>
</label>
<label className="flex items-center justify-between gap-3 bg-white dark:bg-blue-900/30 px-4 py-3">
<div className="flex min-w-0 items-center gap-3">
<span className="text-sm font-semibold text-stone-600 dark:text-stone-300">{dict.myAge}</span>
<select
name="driverAge"
value={filters.driverAge}
onChange={(event) => updateFilter('driverAge', event.target.value)}
className="border-0 bg-transparent text-sm font-semibold text-stone-900 dark:text-stone-100 outline-none"
>
{ageOptions.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
<div
className="relative shrink-0"
onMouseEnter={() => setAgeHelpOpen(true)}
onMouseLeave={() => setAgeHelpOpen(false)}
>
<button
type="button"
aria-label={dict.ageDescription}
onClick={() => setAgeHelpOpen((open) => !open)}
className="text-stone-400 dark:text-stone-500 transition hover:text-stone-600 dark:hover:text-stone-300"
>
<Info className="h-5 w-5" />
</button>
{ageHelpOpen ? (
<div className="absolute bottom-[calc(100%+0.85rem)] right-0 z-30 w-72 rounded-2xl border border-orange-300 dark:border-orange-600 bg-white dark:bg-blue-950 px-4 py-3 text-left text-sm leading-6 text-stone-700 dark:text-stone-200 shadow-[0_16px_40px_rgba(0,0,0,0.14)]">
{dict.ageDescription}
<span className="absolute right-6 top-full h-3 w-3 -translate-y-1/2 rotate-45 border-b border-r border-orange-300 dark:border-orange-600 bg-white dark:bg-blue-950" />
</div>
) : null}
</div>
</label>
<button
type="submit"
className="min-h-[76px] rounded-b-[1.35rem] bg-orange-600 px-6 text-sm font-black uppercase tracking-[0.08em] text-white transition hover:bg-orange-700 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300 lg:rounded-b-none lg:rounded-r-[1.35rem]"
>
{dict.search}
</button>
</div>
</form>
</div>
)
}
@@ -0,0 +1,112 @@
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 (
<section>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.availableVehicles}</h2>
<p className="text-sm text-stone-500 dark:text-stone-400">{vehicles.length} {dict.listings}</p>
</div>
<div className="mt-4 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{vehicles.map((vehicle) => (
<article key={vehicle.id} className="card">
<div className="aspect-[16/10] overflow-hidden rounded-t-[2rem] bg-stone-100 dark:bg-blue-900/30">
{vehicle.photos[0] ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={vehicle.photos[0]} alt={`${vehicle.make} ${vehicle.model}`} className="h-full w-full object-cover" />
) : null}
</div>
<div className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="truncate text-xs uppercase tracking-[0.16em] text-stone-500 dark:text-stone-400">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</p>
<h3 className="mt-2 truncate text-lg font-bold text-stone-900 dark:text-stone-100">{vehicle.make} {vehicle.model}</h3>
<p className="mt-1 truncate text-sm text-stone-500 dark:text-stone-400">{vehicle.year} · {vehicle.category}</p>
</div>
<span className={`shrink-0 rounded-full px-3 py-1 text-xs font-semibold ${vehicle.availability === false ? 'bg-rose-100 text-rose-700 dark:bg-rose-950/50 dark:text-rose-400' : 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400'}`}>
{getStatusCopy(vehicle, dict)}
</span>
</div>
{vehicle.availability === false && (
<p className="mt-3 text-sm text-stone-500 dark:text-stone-400">
{vehicle.nextAvailableAt ? `${dict.availableFrom} ${formatAvailabilityDate(vehicle.nextAvailableAt)}` : dict.unavailableNoDate}
</p>
)}
<div className="mt-4 flex items-end justify-between gap-3">
<div className="min-w-0">
<p className="text-sm text-stone-500 dark:text-stone-400">{dict.from}</p>
<p className="truncate text-xl font-black text-stone-900 dark:text-stone-100">{formatCents(vehicle.dailyRate, language)}</p>
</div>
<Link
href={`/explore/${vehicle.company.slug}/vehicles/${vehicle.id}`}
className="shrink-0 rounded-full bg-blue-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400"
>
{dict.viewVehicle}
</Link>
</div>
</div>
</article>
))}
{vehicles.length === 0 && (
<div className="card p-6 text-sm text-stone-500 dark:text-stone-400 md:col-span-2 xl:col-span-3">{dict.vehicleUnavailable}</div>
)}
</div>
</section>
)
}
@@ -0,0 +1,159 @@
import Link from 'next/link'
import { marketplaceFetchOrDefault } from '@/lib/api'
import { getMarketplaceLanguage } 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 getMarketplaceLanguage()
const dict = {
en: {
unavailable: 'Marketplace unavailable',
unavailableTitle: 'Company details are temporarily unavailable.',
unavailableBody: 'The marketplace API is running, but the database is not reachable in this local environment.',
backToExplore: 'Back to explore',
partner: 'Marketplace 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: 'Marketplace indisponible',
unavailableTitle: "Les détails de l'entreprise sont temporairement indisponibles.",
unavailableBody: "L'API marketplace fonctionne, mais la base de données n'est pas accessible dans cet environnement local.",
backToExplore: "Retour à l'exploration",
partner: 'Partenaire marketplace',
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 marketplaceFetchOrDefault<CompanyProfile | null>(`/marketplace/${params.slug}`, null)
if (!company) {
return (
<main className="site-page">
<div className="site-section">
<section className="card p-8">
<p className="text-sm uppercase tracking-[0.18em] text-orange-700 dark:text-orange-400">{dict.unavailable}</p>
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
<p className="mt-3 text-stone-600 dark:text-stone-400">{dict.unavailableBody}</p>
<div className="mt-6">
<Link href="/explore" className="rounded-full bg-blue-900 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400">{dict.backToExplore}</Link>
</div>
</section>
</div>
</main>
)
}
return (
<main className="site-page">
<div className="site-section space-y-8">
<section className="card p-8">
<p className="text-sm uppercase tracking-[0.18em] text-orange-700 dark:text-orange-400">{company.brand?.publicCity ?? dict.partner}</p>
<h1 className="mt-3 text-4xl font-black text-stone-900 dark:text-stone-100">{company.brand?.displayName ?? company.name}</h1>
<p className="mt-3 text-stone-600 dark:text-stone-400">{company.brand?.publicCountry ?? ''}</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-stone-500 dark:text-stone-400">
<span>{company.vehicles.length} {dict.vehicles}</span>
<span>{company.offers.length} {dict.offers}</span>
<span>{dict.rating} {company.brand?.marketplaceRating?.toFixed(1) ?? dict.new}</span>
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.currentOffers}</h2>
<div className="mt-4 grid gap-4 md:grid-cols-2">
{company.offers.map((offer) => (
<div key={offer.id} className="card p-5">
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">{offer.title}</p>
<p className="mt-2 text-3xl font-black text-orange-700 dark:text-orange-400">{offer.discountValue}%</p>
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">{dict.validUntil} {new Date(offer.validUntil).toLocaleDateString()}</p>
</div>
))}
{company.offers.length === 0 && <div className="card p-6 text-sm text-stone-500 dark:text-stone-400">{dict.noOffers}</div>}
</div>
</section>
<section>
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.fleet}</h2>
<div className="mt-4 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{company.vehicles.map((vehicle) => (
<article key={vehicle.id} className="card">
<div className="aspect-[16/10] overflow-hidden rounded-t-[2rem] bg-stone-100 dark:bg-blue-900/30">
{vehicle.photos[0] ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={vehicle.photos[0]} alt={`${vehicle.make} ${vehicle.model}`} className="h-full w-full object-cover" />
) : null}
</div>
<div className="p-4">
<h3 className="truncate text-lg font-bold text-stone-900 dark:text-stone-100">{vehicle.make} {vehicle.model}</h3>
<p className="mt-1 truncate text-sm text-stone-500 dark:text-stone-400">{vehicle.category}</p>
{!vehicle.availability && (
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">
{vehicle.nextAvailableAt ? `${dict.availableFrom} ${new Date(vehicle.nextAvailableAt).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}` : dict.unavailableNoDate}
</p>
)}
<div className="mt-4 flex items-end justify-between gap-3">
<p className="min-w-0 truncate text-xl font-black text-stone-900 dark:text-stone-100">{formatCurrency(vehicle.dailyRate, 'MAD', language)}</p>
<Link href={`/explore/${params.slug}/vehicles/${vehicle.id}`} className="shrink-0 rounded-full bg-blue-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400">{dict.viewVehicle}</Link>
</div>
</div>
</article>
))}
</div>
</section>
</div>
</main>
)
}
@@ -0,0 +1,283 @@
import Link from 'next/link'
import { marketplaceFetchOrDefault } from '@/lib/api'
import { getMarketplaceLanguage } 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 getMarketplaceLanguage()
const dict = {
en: {
unavailable: 'Marketplace unavailable',
unavailableTitle: 'Vehicle details are temporarily unavailable.',
unavailableBody: 'The marketplace 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: 'Marketplace indisponible',
unavailableTitle: 'Les détails du véhicule sont temporairement indisponibles.',
unavailableBody: "Le frontend marketplace fonctionne, mais la base de données n'est pas accessible pour le moment.",
backToFleet: 'Retour à la flotte',
backToExplore: 'Retour à lexploration',
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 marketplaceFetchOrDefault<VehicleDetail | null>(`/marketplace/${params.slug}/vehicles/${params.id}`, null)
if (!vehicle) {
return (
<main className="site-page">
<div className="site-section">
<section className="card p-8">
<p className="text-sm uppercase tracking-[0.18em] text-orange-700 dark:text-orange-400">{dict.unavailable}</p>
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
<p className="mt-3 text-stone-600 dark:text-stone-400">{dict.unavailableBody}</p>
<div className="mt-6 flex gap-3">
<Link href={`/explore/${params.slug}`} className="rounded-full bg-blue-900 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:text-white dark:hover:bg-orange-400">{dict.backToFleet}</Link>
<Link href="/explore" className="rounded-full border border-stone-200 px-5 py-2.5 text-sm font-semibold text-stone-700 transition hover:border-stone-400 dark:border-blue-800 dark:text-stone-300 dark:hover:border-stone-500">{dict.backToExplore}</Link>
</div>
</section>
</div>
</main>
)
}
const availabilityDate = vehicle.nextAvailableAt
? new Date(vehicle.nextAvailableAt).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })
: null
const [heroPhoto, ...galleryPhotos] = vehicle.photos
return (
<main className="site-page">
<div className="site-section space-y-8">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm text-stone-500 dark:text-stone-400">
<Link href="/explore" className="hover:text-stone-900 dark:hover:text-stone-100 transition">{dict.backToExplore}</Link>
<span>/</span>
<Link href={`/explore/${params.slug}`} className="hover:text-stone-900 dark:hover:text-stone-100 transition">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</Link>
<span>/</span>
<span className="text-stone-900 dark:text-stone-100 font-medium">{vehicle.make} {vehicle.model}</span>
</nav>
<div className="grid gap-8 lg:grid-cols-[1fr_380px]">
{/* Left — photos + specs */}
<div className="space-y-6">
{/* Hero photo */}
{heroPhoto ? (
<div className="overflow-hidden rounded-3xl bg-stone-100 dark:bg-blue-900/30 aspect-[16/9]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={heroPhoto} alt={`${vehicle.make} ${vehicle.model}`} className="h-full w-full object-cover" />
</div>
) : (
<div className="overflow-hidden rounded-3xl bg-stone-100 dark:bg-blue-900/30 aspect-[16/9] flex items-center justify-center text-sm text-stone-400">{dict.noPhotos}</div>
)}
{/* Gallery */}
{galleryPhotos.length > 0 && (
<div>
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-stone-400 dark:text-stone-500">{dict.photos}</p>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{galleryPhotos.map((photo, index) => (
<div key={`${photo}-${index}`} className="overflow-hidden rounded-2xl bg-stone-100 dark:bg-blue-900/30 aspect-[4/3]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={photo} alt={`${vehicle.make} ${vehicle.model} ${index + 2}`} className="h-full w-full object-cover" />
</div>
))}
</div>
</div>
)}
{/* Specs */}
<div className="card p-6 space-y-4">
<h2 className="text-lg font-bold text-stone-900 dark:text-stone-100">{dict.specs}</h2>
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3">
<div>
<dt className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.year}</dt>
<dd className="mt-1 text-base font-semibold text-stone-900 dark:text-stone-100">{vehicle.year}</dd>
</div>
<div>
<dt className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.category}</dt>
<dd className="mt-1 text-base font-semibold text-stone-900 dark:text-stone-100">{vehicle.category}</dd>
</div>
{vehicle.seats > 0 && (
<div>
<dt className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.seats}</dt>
<dd className="mt-1 text-base font-semibold text-stone-900 dark:text-stone-100">{vehicle.seats}</dd>
</div>
)}
{vehicle.transmission && (
<div>
<dt className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.transmission}</dt>
<dd className="mt-1 text-base font-semibold text-stone-900 dark:text-stone-100">{vehicle.transmission}</dd>
</div>
)}
{vehicle.fuelType && (
<div>
<dt className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.fuel}</dt>
<dd className="mt-1 text-base font-semibold text-stone-900 dark:text-stone-100">{vehicle.fuelType}</dd>
</div>
)}
</dl>
{vehicle.features.length > 0 && (
<div className="pt-2 border-t border-stone-100 dark:border-blue-900">
<p className="text-xs font-medium uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500 mb-3">{dict.features}</p>
<div className="flex flex-wrap gap-2">
{vehicle.features.map((feature) => (
<span key={feature} className="rounded-full bg-stone-100 dark:bg-blue-900/30 px-3 py-1 text-xs font-medium text-stone-700 dark:text-stone-300">{feature}</span>
))}
</div>
</div>
)}
</div>
{/* Contact */}
{(vehicle.company.brand?.publicPhone || vehicle.company.brand?.whatsappNumber) && (
<div className="card p-6 space-y-3">
<h2 className="text-lg font-bold text-stone-900 dark:text-stone-100">{dict.contact}</h2>
<p className="text-sm text-stone-500 dark:text-stone-400">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</p>
<div className="flex flex-wrap gap-3">
{vehicle.company.brand?.publicPhone && (
<a href={`tel:${vehicle.company.brand.publicPhone}`} className="inline-flex items-center gap-2 rounded-full border border-stone-300 dark:border-blue-800 px-4 py-2 text-sm font-semibold text-stone-700 dark:text-stone-300 hover:bg-stone-100 dark:hover:bg-blue-900/40 transition">
{dict.phone}: {vehicle.company.brand.publicPhone}
</a>
)}
{vehicle.company.brand?.whatsappNumber && (
<a href={`https://wa.me/${vehicle.company.brand.whatsappNumber.replace(/\D/g, '')}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 rounded-full bg-orange-600 px-4 py-2 text-sm font-semibold text-white hover:bg-orange-700 transition">
{dict.whatsapp}
</a>
)}
</div>
</div>
)}
</div>
{/* Right — sticky booking panel */}
<aside className="space-y-4">
<div className="card p-6 space-y-4 lg:sticky lg:top-20">
<p className="text-xs uppercase tracking-[0.18em] text-stone-500 dark:text-stone-400">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</p>
<h1 className="text-3xl font-black text-stone-900 dark:text-stone-100">{vehicle.make} {vehicle.model}</h1>
<p className="text-stone-500 dark:text-stone-400">{vehicle.year} · {vehicle.category}</p>
<p className="text-3xl font-black text-stone-900 dark:text-stone-100">
{formatCurrency(vehicle.dailyRate, 'MAD', language)}
<span className="text-base font-medium text-stone-500 dark:text-stone-400"> {dict.perDay}</span>
</p>
{!vehicle.availability && (
<div className="rounded-2xl border border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-950/40 px-4 py-3 text-sm text-orange-800 dark:text-orange-300">
{availabilityDate ? `${dict.availableFrom} ${availabilityDate}` : dict.unavailableNoDate}
</div>
)}
{vehicle.availability && (
<BookingForm
vehicleId={vehicle.id}
companySlug={params.slug}
dailyRate={vehicle.dailyRate}
pickupLocations={vehicle.pickupLocations}
allowDifferentDropoff={vehicle.allowDifferentDropoff}
dropoffLocations={vehicle.dropoffLocations}
/>
)}
<Link
href={`/explore/${params.slug}`}
className="block rounded-full border border-stone-300 dark:border-blue-800 px-6 py-3 text-center text-sm font-semibold text-stone-700 dark:text-stone-300 hover:bg-stone-100 dark:hover:bg-blue-900/40 transition"
>
{dict.backToFleet}
</Link>
</div>
</aside>
</div>
</div>
</main>
)
}
@@ -0,0 +1,329 @@
import Link from 'next/link'
import { marketplaceFetchOrDefault } from '@/lib/api'
import { getMarketplaceLanguage } 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<Record<string, string | string[] | undefined>> }) {
const resolvedSearchParams = await searchParams
const language = await getMarketplaceLanguage()
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 marketplace offers',
company: 'Company',
validUntil: 'Valid until',
offersUnavailable: 'Marketplace 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',
marketplacePartners: 'Marketplace partners',
marketplacePartner: 'Marketplace 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 marketplace mises en avant',
company: 'Entreprise',
validUntil: "Valable jusqu'au",
offersUnavailable: 'Les offres marketplace 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',
marketplacePartners: 'Partenaires marketplace',
marketplacePartner: 'Partenaire marketplace',
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: 'تصفح حسب الشركة',
marketplacePartners: 'شركاء السوق',
marketplacePartner: 'شريك في السوق',
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([
marketplaceFetchOrDefault<Offer[]>('/marketplace/offers', []),
marketplaceFetchOrDefault<Vehicle[]>(`/marketplace/search?${query.toString()}`, []),
marketplaceFetchOrDefault<CompanyCard[]>('/marketplace/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 (
<main className="site-page">
<div className="site-section space-y-10">
<section className="site-panel-contrast bg-[linear-gradient(135deg,#0f2460,#1a3a8f)] dark:bg-[linear-gradient(135deg,#0a1735,#112d6e)] px-8 py-12">
<p className="site-kicker text-orange-300 dark:text-orange-300">{dict.kicker}</p>
<h1 className="mt-4 text-4xl font-black tracking-tight">{dict.title}</h1>
<p className="mt-4 max-w-2xl text-blue-100 dark:text-blue-100">{dict.body}</p>
<ExploreSearchForm
pickupLocation={pickupLocation}
dropoffLocation={dropoffLocation}
dropoffMode={dropoffMode}
pickupDate={pickupDate}
pickupTime={pickupTime}
returnDate={returnDate}
returnTime={returnTime}
promoCode={promoCode}
driverAge={driverAge}
cities={cities}
dict={dict}
/>
</section>
<section>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.currentDeals}</h2>
<p className="text-sm text-stone-500 dark:text-stone-400">{dict.featuredOffers}</p>
</div>
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{offers.map((offer) => (
<Link key={offer.id} href={`/explore/${offer.company.brand?.subdomain ?? ''}#offers`} className="card flex flex-col p-5 transition-colors hover:border-orange-300 dark:hover:border-orange-500">
<p className="truncate text-xs uppercase tracking-[0.18em] text-orange-700 dark:text-orange-400">{offer.company.brand?.displayName ?? dict.company}</p>
<h3 className="mt-3 truncate text-lg font-semibold text-stone-900 dark:text-stone-100">{offer.title}</h3>
<p className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{offer.discountValue}%</p>
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">{dict.validUntil} {new Date(offer.validUntil).toLocaleDateString()}</p>
</Link>
))}
{offers.length === 0 && <div className="card p-6 text-sm text-stone-500 dark:text-stone-400 md:col-span-2 xl:col-span-4">{dict.offersUnavailable}</div>}
</div>
</section>
<ExploreVehicleGrid vehicles={vehicles} dict={dict} language={language} />
<section>
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.browseByCategory}</h2>
<div className="mt-4 flex flex-wrap gap-3">
{dict.categories.map((categoryName, index) => (
<Link key={CATEGORY_VALUES[index] ?? categoryName} href={`/explore?category=${CATEGORY_VALUES[index]}`} className="rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-700 transition hover:border-stone-400 dark:border-blue-800 dark:bg-blue-950 dark:text-stone-300 dark:hover:border-stone-500">
{categoryName}
</Link>
))}
</div>
</section>
<section>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.browseByCompany}</h2>
<p className="text-sm text-stone-500 dark:text-stone-400">{dict.marketplacePartners}</p>
</div>
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{companies.map((company) => (
<Link key={company.id} href={`/explore/${company.brand?.subdomain ?? ''}`} className="card flex flex-col p-5 transition-colors hover:border-orange-300 dark:hover:border-orange-500">
<p className="truncate text-xs uppercase tracking-[0.18em] text-stone-500 dark:text-stone-400">{company.brand?.publicCity ?? dict.marketplacePartner}</p>
<h3 className="mt-3 truncate text-lg font-semibold text-stone-900 dark:text-stone-100">{company.brand?.displayName ?? dict.rentalCompany}</h3>
<p className="mt-2 text-sm text-stone-500 dark:text-stone-400">{dict.rating} {company.brand?.marketplaceRating?.toFixed(1) ?? dict.new}</p>
<p className="mt-1 text-sm text-stone-500 dark:text-stone-400">{company._count.vehicles} {dict.publishedVehicles}</p>
<span className="mt-4 inline-flex self-start rounded-full bg-blue-900 px-4 py-2 text-sm font-semibold text-white dark:bg-orange-500 dark:text-white">{dict.viewFleet}</span>
</Link>
))}
</div>
</section>
</div>
</main>
)
}