d7fb7b7a7b
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
113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|