fixing platform admin
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
'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 à explorer',
|
||||
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: 'Email',
|
||||
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('/renter/sign-in')
|
||||
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('/renter/sign-in')
|
||||
} else {
|
||||
setErrorMsg(json?.message ?? dict.loadProfile)
|
||||
setStatus('error')
|
||||
}
|
||||
return
|
||||
}
|
||||
setProfile(json?.data ?? json)
|
||||
setStatus('ready')
|
||||
})
|
||||
.catch(() => {
|
||||
setErrorMsg(dict.reachServer)
|
||||
setStatus('error')
|
||||
})
|
||||
}, [router])
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('renter_token')
|
||||
router.push('/explore')
|
||||
}
|
||||
|
||||
// Loading skeleton
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-50 py-10">
|
||||
<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>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-50 py-10">
|
||||
<div className="shell">
|
||||
<div className="card p-8 text-center">
|
||||
<p className="text-sm text-rose-600">{errorMsg}</p>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="mt-4 rounded-full bg-stone-900 px-6 py-2.5 text-sm font-semibold text-white"
|
||||
>
|
||||
{dict.backToExplore}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const savedCompanies = profile?.savedCompanies ?? []
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-50 py-10">
|
||||
<div className="shell space-y-8">
|
||||
{/* Header */}
|
||||
<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-amber-700">
|
||||
{dict.renterDashboard}
|
||||
</p>
|
||||
<h1 className="mt-1 text-3xl font-black tracking-tight text-stone-900">
|
||||
{dict.welcome} {profile?.firstName}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/explore"
|
||||
className="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>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="rounded-full bg-stone-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-stone-700"
|
||||
>
|
||||
{dict.logout}
|
||||
</button>
|
||||
</div>
|
||||
</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-amber-300 transition-colors">
|
||||
<p className="text-sm font-semibold text-stone-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-stone-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-stone-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-stone-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-stone-900 px-6 py-2.5 text-sm font-semibold text-white hover:bg-stone-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-amber-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-amber-100 text-amber-700 font-bold text-sm">
|
||||
{company.brand?.displayName?.charAt(0) ?? '?'}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-stone-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>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user