Files
carmanagement/storefront/src/app/renter/profile/page.tsx
T
root 8aab968e09
Build & Deploy / Build & Push Docker Image (push) Successful in 1m2s
Test / Type Check (all packages) (push) Failing after 28s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Storefront Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
Build & Deploy / Deploy to VPS (push) Successful in 3s
refactor: rename marketplace to storefront across the entire monorepo
Comprehensive rename of all marketplace references to storefront:
- API module: apps/api/src/modules/marketplace/ → storefront/
- Components: MarketplaceHeader → StorefrontHeader, MarketplaceShell →
  StorefrontShell, MarketplaceFooter → StorefrontFooter
- Types: marketplace-homepage.ts → storefront-homepage.ts
- Test files: employee-marketplace-* → employee-storefront-*
- All source code identifiers, imports, route paths, and strings
- Documentation (docs/), CI config (.gitlab-ci.yml), scripts
- Dashboard, admin, storefront workspace references
- Prisma field names preserved (isListedOnMarketplace, marketplaceRating,
  marketplaceFunnelEvent) as they map to database schema

Validation:
- API type-check: 0 errors
- Storefront type-check: 0 errors
- Dashboard type-check: 0 errors
- Full monorepo type-check: only pre-existing admin TS18046
2026-06-28 01:14:46 -04:00

319 lines
12 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 { useRouter } from 'next/navigation'
import {
RenterAuthError,
clearRenterSession,
loadRenterPreferences,
loadRenterProfile,
updateRenterPreferences,
updateRenterProfile,
type RenterPreference,
type RenterProfile,
} from '@/lib/renter'
import { useStorefrontPreferences } from '@/components/StorefrontShell'
type Status = 'loading' | 'ready' | 'error'
const RENTER_EVENTS = ['BOOKING_CONFIRMED', 'BOOKING_REMINDER', 'RETURN_REMINDER', 'REFUND_ISSUED', 'NEW_OFFER']
const RENTER_CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'PUSH', 'IN_APP']
export default function RenterProfilePage() {
const router = useRouter()
const { language } = useStorefrontPreferences()
const dict = {
en: {
profile: 'Profile',
loading: 'Loading profile…',
loadFailed: 'Could not load profile.',
yourProfile: 'Your profile',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phone: 'Phone',
locale: 'Locale',
currency: 'Currency',
saving: 'Saving…',
saveProfile: 'Save profile',
verification: 'Verification',
verified: 'Email verified',
pending: 'Email verification is still pending.',
notificationPrefs: 'Notification preferences',
notificationBody: 'Choose how reminders, offers, and booking updates reach you.',
savePreferences: 'Save preferences',
event: 'Event',
back: 'Back to dashboard',
savedCompanies: 'Saved companies',
notifications: 'Notifications',
logout: 'Log out',
renter: 'Renter',
saveFailed: 'Could not save profile.',
prefFailed: 'Could not save preferences.',
},
fr: {
profile: 'Profil',
loading: 'Chargement du profil…',
loadFailed: 'Impossible de charger le profil.',
yourProfile: 'Votre profil',
firstName: 'Prénom',
lastName: 'Nom',
email: 'Email',
phone: 'Téléphone',
locale: 'Langue',
currency: 'Devise',
saving: 'Enregistrement…',
saveProfile: 'Enregistrer le profil',
verification: 'Vérification',
verified: 'Email vérifié',
pending: 'La vérification de lemail est encore en attente.',
notificationPrefs: 'Préférences de notification',
notificationBody: 'Choisissez comment les rappels, offres et mises à jour vous parviennent.',
savePreferences: 'Enregistrer les préférences',
event: 'Événement',
back: 'Retour au dashboard',
savedCompanies: 'Entreprises sauvegardées',
notifications: 'Notifications',
logout: 'Déconnexion',
renter: 'Client',
saveFailed: 'Impossible denregistrer le profil.',
prefFailed: 'Impossible denregistrer les préférences.',
},
ar: {
profile: 'الملف الشخصي',
loading: 'جارٍ تحميل الملف الشخصي…',
loadFailed: 'تعذر تحميل الملف الشخصي.',
yourProfile: 'ملفك الشخصي',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
locale: 'اللغة',
currency: 'العملة',
saving: 'جارٍ الحفظ…',
saveProfile: 'حفظ الملف الشخصي',
verification: 'التحقق',
verified: 'تم التحقق من البريد الإلكتروني',
pending: 'التحقق من البريد الإلكتروني ما زال قيد الانتظار.',
notificationPrefs: 'تفضيلات الإشعارات',
notificationBody: 'اختر كيف تصلك التذكيرات والعروض وتحديثات الحجز.',
savePreferences: 'حفظ التفضيلات',
event: 'الحدث',
back: 'العودة إلى اللوحة',
savedCompanies: 'الشركات المحفوظة',
notifications: 'الإشعارات',
logout: 'تسجيل الخروج',
renter: 'المستأجر',
saveFailed: 'تعذر حفظ الملف الشخصي.',
prefFailed: 'تعذر حفظ التفضيلات.',
},
}[language]
const [profile, setProfile] = useState<RenterProfile | null>(null)
const [preferences, setPreferences] = useState<Record<string, boolean>>({})
const [status, setStatus] = useState<Status>('loading')
const [errorMsg, setErrorMsg] = useState('')
const [saving, setSaving] = useState(false)
useEffect(() => {
Promise.all([loadRenterProfile(), loadRenterPreferences()])
.then(([profileData, preferenceData]) => {
setProfile(profileData)
setPreferences(
Object.fromEntries(
preferenceData.map((item) => [`${item.notificationType}:${item.channel}`, item.enabled]),
),
)
setStatus('ready')
})
.catch((err) => {
if (err instanceof RenterAuthError) {
router.replace('/')
return
}
setErrorMsg(err instanceof Error ? err.message : dict.loadFailed)
setStatus('error')
})
}, [router])
if (status === 'loading') {
return <div className="shell"><div className="card p-8 text-sm text-stone-500">{dict.loading}</div></div>
}
if (status === 'error') {
return <div className="shell"><ErrorCard message={errorMsg} /></div>
}
return (
<div className="shell space-y-6">
<div className="card p-8">
<h1 className="text-3xl font-black tracking-tight text-blue-900">{dict.yourProfile}</h1>
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<EditableField label={dict.firstName} value={profile?.firstName ?? ''} onChange={(value) => setProfile((current) => current ? { ...current, firstName: value } : current)} />
<EditableField label={dict.lastName} value={profile?.lastName ?? ''} onChange={(value) => setProfile((current) => current ? { ...current, lastName: value } : current)} />
<Field label={dict.email} value={profile?.email} />
<EditableField label={dict.phone} value={profile?.phone ?? ''} onChange={(value) => setProfile((current) => current ? { ...current, phone: value } : current)} />
<SelectField
label={dict.locale}
value={profile?.preferredLocale || 'en'}
options={['en', 'fr', 'ar']}
onChange={(value) => setProfile((current) => current ? { ...current, preferredLocale: value } : current)}
/>
<SelectField
label={dict.currency}
value="MAD"
options={['MAD']}
onChange={() => {}}
/>
</div>
<div className="mt-8 flex justify-end">
<button
type="button"
onClick={async () => {
if (!profile) return
setSaving(true)
setErrorMsg('')
try {
const updated = await updateRenterProfile({
firstName: profile.firstName,
lastName: profile.lastName,
phone: profile.phone,
preferredLocale: profile.preferredLocale,
preferredCurrency: profile.preferredCurrency,
})
setProfile(updated)
} catch (err) {
setErrorMsg(err instanceof Error ? err.message : dict.saveFailed)
} finally {
setSaving(false)
}
}}
className="rounded-full bg-orange-600 px-5 py-2 text-sm font-semibold text-white"
>
{saving ? dict.saving : dict.saveProfile}
</button>
</div>
<div className="mt-8 rounded-2xl border border-stone-200 bg-stone-50 p-5">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-stone-500">{dict.verification}</p>
<p className="mt-2 text-sm text-stone-700">
{profile?.emailVerified ? dict.verified : dict.pending}
</p>
</div>
</div>
<div className="card p-8">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-blue-900">{dict.notificationPrefs}</h2>
<p className="mt-2 text-sm text-stone-600">{dict.notificationBody}</p>
</div>
<button
type="button"
onClick={async () => {
setSaving(true)
setErrorMsg('')
try {
const payload: RenterPreference[] = Object.entries(preferences).map(([key, enabled]) => {
const [notificationType, channel] = key.split(':')
return { notificationType, channel, enabled }
})
await updateRenterPreferences(payload)
} catch (err) {
setErrorMsg(err instanceof Error ? err.message : dict.prefFailed)
} finally {
setSaving(false)
}
}}
className="rounded-full bg-orange-600 px-5 py-2 text-sm font-semibold text-white"
>
{saving ? dict.saving : dict.savePreferences}
</button>
</div>
<div className="mt-6 overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-stone-50">
<tr>
<th className="px-4 py-3 text-left font-semibold text-stone-500">{dict.event}</th>
{RENTER_CHANNELS.map((channel) => (
<th key={channel} className="px-4 py-3 text-center font-semibold text-stone-500">
{channel}
</th>
))}
</tr>
</thead>
<tbody>
{RENTER_EVENTS.map((eventName) => (
<tr key={eventName} className="border-t border-stone-100">
<td className="px-4 py-3 font-medium text-blue-900">{eventName.replaceAll('_', ' ')}</td>
{RENTER_CHANNELS.map((channel) => (
<td key={`${eventName}-${channel}`} className="px-4 py-3 text-center">
<input
type="checkbox"
checked={preferences[`${eventName}:${channel}`] ?? true}
onChange={(event) =>
setPreferences((current) => ({
...current,
[`${eventName}:${channel}`]: event.target.checked,
}))
}
className="h-4 w-4 rounded border-stone-300 text-orange-600"
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
function Field({ label, value }: { label: string; value?: string | null }) {
return (
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-stone-400">{label}</p>
<p className="mt-2 text-sm font-medium text-blue-900 break-all">{value}</p>
</div>
)
}
function EditableField({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) {
return (
<label className="block">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-stone-400">{label}</p>
<input
value={value}
onChange={(event) => onChange(event.target.value)}
className="mt-2 w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm font-medium text-blue-900"
/>
</label>
)
}
function SelectField({ label, value, options, onChange }: { label: string; value: string; options: string[]; onChange: (value: string) => void }) {
return (
<label className="block">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-stone-400">{label}</p>
<select
value={value}
onChange={(event) => onChange(event.target.value)}
className="mt-2 w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm font-medium text-blue-900"
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
)
}
function ErrorCard({ message }: { message: string }) {
return (
<div className="card p-8 text-center">
<p className="text-sm text-rose-600">{message}</p>
</div>
)
}