Files
carmanagement/apps/marketplace/src/app/renter/dashboard/page.tsx
T
2026-05-24 23:58:54 -04:00

311 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
interface SavedCompany {
id: string
brand?: {
displayName: string
subdomain: string
logoUrl?: string | null
} | null
}
interface RenterProfile {
id: string
firstName: string
lastName: string
email: string
phone?: string | null
savedCompanies?: SavedCompany[]
}
type Status = 'loading' | 'ready' | 'error'
export default function RenterDashboardPage() {
const router = useRouter()
const { language } = useMarketplacePreferences()
const dict = {
en: {
loadProfile: 'Could not load profile.',
reachServer: 'Could not reach the server.',
backToExplore: 'Back to explore',
renterDashboard: 'Renter Dashboard',
welcome: 'Welcome back,',
exploreVehicles: 'Explore vehicles',
logout: 'Log out',
cards: [
['Profile', 'Review your account details and preferences.'],
['Saved companies', 'Jump back into companies you bookmarked.'],
['Notifications', 'See booking updates and in-app alerts.'],
],
yourProfile: 'Your profile',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phone: 'Phone',
savedCompanies: 'Saved companies',
noneSaved: 'None saved yet',
saved: 'saved',
saveCompaniesPrompt: 'Save companies you like while browsing the marketplace.',
startExploring: 'Start exploring',
rentalCompany: 'Rental company',
},
fr: {
loadProfile: 'Impossible de charger le profil.',
reachServer: 'Impossible de joindre le serveur.',
backToExplore: 'Retour à lexploration',
renterDashboard: 'Espace client',
welcome: 'Bon retour,',
exploreVehicles: 'Explorer les véhicules',
logout: 'Déconnexion',
cards: [
['Profil', 'Consultez vos informations et préférences.'],
['Entreprises sauvegardées', 'Revenez vers les entreprises que vous avez enregistrées.'],
['Notifications', 'Consultez les mises à jour de réservation et les alertes.'],
],
yourProfile: 'Votre profil',
firstName: 'Prénom',
lastName: 'Nom',
email: 'E-mail',
phone: 'Téléphone',
savedCompanies: 'Entreprises sauvegardées',
noneSaved: 'Aucune pour le moment',
saved: 'sauvegardées',
saveCompaniesPrompt: 'Enregistrez les entreprises que vous aimez en parcourant la marketplace.',
startExploring: 'Commencer à explorer',
rentalCompany: 'Société de location',
},
ar: {
loadProfile: 'تعذر تحميل الملف الشخصي.',
reachServer: 'تعذر الوصول إلى الخادم.',
backToExplore: 'العودة إلى الاستكشاف',
renterDashboard: 'بوابة المستأجر',
welcome: 'مرحباً بعودتك،',
exploreVehicles: 'استكشف السيارات',
logout: 'تسجيل الخروج',
cards: [
['الملف الشخصي', 'راجع بيانات حسابك وتفضيلاتك.'],
['الشركات المحفوظة', 'ارجع بسرعة إلى الشركات التي حفظتها.'],
['الإشعارات', 'اطلع على تحديثات الحجز والتنبيهات داخل التطبيق.'],
],
yourProfile: 'ملفك الشخصي',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
savedCompanies: 'الشركات المحفوظة',
noneSaved: 'لا يوجد شيء محفوظ بعد',
saved: 'محفوظة',
saveCompaniesPrompt: 'احفظ الشركات التي تعجبك أثناء تصفح السوق.',
startExploring: 'ابدأ الاستكشاف',
rentalCompany: 'شركة تأجير',
},
}[language]
const [profile, setProfile] = useState<RenterProfile | null>(null)
const [status, setStatus] = useState<Status>('loading')
const [errorMsg, setErrorMsg] = useState('')
useEffect(() => {
const token = localStorage.getItem('renter_token')
if (!token) {
router.replace('/')
return
}
fetch(`${API_BASE}/auth/renter/me`, {
headers: { Authorization: `Bearer ${token}` },
})
.then(async (res) => {
const json = await res.json().catch(() => null)
if (!res.ok) {
if (res.status === 401) {
localStorage.removeItem('renter_token')
router.replace('/')
} else {
setErrorMsg(json?.message ?? dict.loadProfile)
setStatus('error')
}
return
}
setProfile(json?.data ?? json)
setStatus('ready')
})
.catch(() => {
setErrorMsg(dict.reachServer)
setStatus('error')
})
}, [router])
if (status === 'loading') {
return (
<div className="shell">
<div className="animate-pulse space-y-6">
<div className="h-8 w-48 rounded-xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
</div>
</div>
)
}
if (status === 'error') {
return (
<div className="shell">
<div className="card p-8 text-center">
<p className="text-sm text-rose-600">{errorMsg}</p>
<Link
href="/explore"
className="mt-4 inline-flex rounded-full bg-orange-600 px-6 py-2.5 text-sm font-semibold text-white"
>
{dict.backToExplore}
</Link>
</div>
</div>
)
}
const savedCompanies = profile?.savedCompanies ?? []
return (
<div className="shell space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-orange-700">
{dict.renterDashboard}
</p>
<h1 className="mt-1 text-3xl font-black tracking-tight text-blue-900">
{dict.welcome} {profile?.firstName}
</h1>
</div>
<Link
href="/explore"
className="self-start rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700 shadow-sm hover:border-stone-300"
>
{dict.exploreVehicles}
</Link>
</div>
<section className="grid gap-4 sm:grid-cols-3">
{[
{ href: '/renter/profile', label: dict.cards[0][0], copy: dict.cards[0][1] },
{ href: '/renter/saved-companies', label: dict.cards[1][0], copy: dict.cards[1][1] },
{ href: '/renter/notifications', label: dict.cards[2][0], copy: dict.cards[2][1] },
].map((item) => (
<Link key={item.href} href={item.href} className="card p-5 hover:border-orange-300 transition-colors">
<p className="text-sm font-semibold text-blue-900">{item.label}</p>
<p className="mt-2 text-sm leading-6 text-stone-500">{item.copy}</p>
</Link>
))}
</section>
{/* Profile card */}
<section className="card p-8">
<h2 className="text-lg font-bold text-blue-900">{dict.yourProfile}</h2>
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{ label: dict.firstName, value: profile?.firstName },
{ label: dict.lastName, value: profile?.lastName },
{ label: dict.email, value: profile?.email },
{ label: dict.phone, value: profile?.phone ?? '—' },
].map(({ label, value }) => (
<div key={label}>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-stone-400">
{label}
</p>
<p className="mt-1.5 text-sm font-medium text-blue-900 break-all">
{value}
</p>
</div>
))}
</div>
</section>
{/* Saved companies */}
<section>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-blue-900">{dict.savedCompanies}</h2>
<p className="text-sm text-stone-500">
{savedCompanies.length === 0
? dict.noneSaved
: `${savedCompanies.length} ${dict.saved}`}
</p>
</div>
{savedCompanies.length === 0 ? (
<div className="card mt-4 flex flex-col items-center gap-4 py-16 text-center">
<svg
className="h-10 w-10 text-stone-300"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
/>
</svg>
<p className="text-sm text-stone-500">
{dict.saveCompaniesPrompt}
</p>
<Link
href="/explore"
className="rounded-full bg-orange-600 px-6 py-2.5 text-sm font-semibold text-white hover:bg-orange-700"
>
{dict.startExploring}
</Link>
</div>
) : (
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{savedCompanies.map((company) => (
<Link
key={company.id}
href={`/explore/${company.brand?.subdomain ?? company.id}`}
className="card flex items-center gap-4 p-5 hover:border-orange-300 transition-colors"
>
{company.brand?.logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={company.brand.logoUrl}
alt={company.brand.displayName}
className="h-10 w-10 rounded-lg object-contain"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 text-orange-700 font-bold text-sm">
{company.brand?.displayName?.charAt(0) ?? '?'}
</div>
)}
<div className="min-w-0">
<p className="truncate font-semibold text-blue-900">
{company.brand?.displayName ?? dict.rentalCompany}
</p>
<p className="mt-0.5 text-xs text-stone-400">
{company.brand?.subdomain ?? ''}
</p>
</div>
<svg
className="ml-auto h-4 w-4 shrink-0 text-stone-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</Link>
))}
</div>
)}
</section>
</div>
)
}