fix and update language for company workspace

This commit is contained in:
root
2026-05-11 23:40:40 -04:00
committed by Administrator
parent 193aeae834
commit 1a39aa8433
43 changed files with 5034 additions and 802 deletions
@@ -44,7 +44,9 @@ interface Dict {
// modal
reserveVehicle: string
pickupDate: string
pickupTime: string
returnDate: string
dropoffTime: string
firstName: string
lastName: string
email: string
@@ -85,7 +87,9 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
email: '',
phone: '',
startDate: today(),
startTime: '10:00',
endDate: tomorrow(),
endTime: '10:00',
notes: '',
})
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle')
@@ -97,7 +101,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
setSelected(vehicle)
setStatus('idle')
setErrorMsg('')
setForm({ firstName: '', lastName: '', email: '', phone: '', startDate: minStart, endDate: minEnd, notes: '' })
setForm({ firstName: '', lastName: '', email: '', phone: '', startDate: minStart, startTime: '10:00', endDate: minEnd, endTime: '10:00', notes: '' })
}
function closeModal() {
@@ -137,8 +141,8 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
lastName: form.lastName,
email: form.email,
phone: form.phone || undefined,
startDate: new Date(form.startDate).toISOString(),
endDate: new Date(form.endDate).toISOString(),
startDate: new Date(`${form.startDate}T${form.startTime}`).toISOString(),
endDate: new Date(`${form.endDate}T${form.endTime}`).toISOString(),
notes: form.notes || undefined,
})
setStatus('success')
@@ -260,7 +264,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
</div>
)}
{/* Dates */}
{/* Dates + Times */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupDate}</label>
@@ -284,6 +288,26 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
className="w-full rounded-xl border border-stone-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-stone-900"
/>
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupTime}</label>
<input
type="time"
required
value={form.startTime}
onChange={set('startTime')}
className="w-full rounded-xl border border-stone-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-stone-900"
/>
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.dropoffTime}</label>
<input
type="time"
required
value={form.endTime}
onChange={set('endTime')}
className="w-full rounded-xl border border-stone-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-stone-900"
/>
</div>
</div>
{/* Estimated total */}
@@ -96,7 +96,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
// modal
reserveVehicle: 'Reserve this vehicle',
pickupDate: 'Pick-up date',
pickupTime: 'Pick-up time',
returnDate: 'Return date',
dropoffTime: 'Drop-off time',
firstName: 'First name',
lastName: 'Last name',
email: 'Email address',
@@ -152,7 +154,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
categories: ['Économie', 'Compacte', 'SUV', 'Luxe', 'Van', 'Camion', 'Électrique'],
reserveVehicle: 'Réserver ce véhicule',
pickupDate: 'Date de prise en charge',
pickupTime: 'Heure de prise en charge',
returnDate: 'Date de retour',
dropoffTime: 'Heure de restitution',
firstName: 'Prénom',
lastName: 'Nom',
email: 'Adresse e-mail',
@@ -208,7 +212,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
categories: ['اقتصادية', 'مدمجة', 'SUV', 'فاخرة', 'فان', 'شاحنة', 'كهربائية'],
reserveVehicle: 'احجز هذه السيارة',
pickupDate: 'تاريخ الاستلام',
pickupTime: 'وقت الاستلام',
returnDate: 'تاريخ الإرجاع',
dropoffTime: 'وقت الإرجاع',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
+198
View File
@@ -0,0 +1,198 @@
'use client'
import { useEffect, useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { Star } from 'lucide-react'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
interface ReviewInfo {
reservationId: string
companyName: string
companyLogoUrl: string | null
vehicle: string
vehiclePhoto: string | null
}
function StarRating({ value, onChange, label }: { value: number; onChange: (v: number) => void; label: string }) {
const [hovered, setHovered] = useState(0)
return (
<div>
<p className="text-sm font-medium text-stone-700 mb-1.5">{label}</p>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
onClick={() => onChange(n)}
onMouseEnter={() => setHovered(n)}
onMouseLeave={() => setHovered(0)}
className="p-0.5 transition-transform hover:scale-110"
aria-label={`${n} star${n > 1 ? 's' : ''}`}
>
<Star
className={`h-8 w-8 transition-colors ${
n <= (hovered || value)
? 'fill-amber-400 text-amber-400'
: 'fill-stone-200 text-stone-200'
}`}
/>
</button>
))}
</div>
</div>
)
}
export default function ReviewPage() {
const params = useSearchParams()
const token = params.get('token') ?? ''
const [info, setInfo] = useState<ReviewInfo | null>(null)
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'already_reviewed' | 'invalid'>('loading')
const [overall, setOverall] = useState(0)
const [vehicle, setVehicle] = useState(0)
const [service, setService] = useState(0)
const [comment, setComment] = useState('')
const [submitState, setSubmitState] = useState<'idle' | 'submitting' | 'done' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
useEffect(() => {
if (!token) { setLoadState('invalid'); return }
fetch(`${API_BASE}/marketplace/review/${token}`)
.then(async (r) => {
if (r.status === 409) { setLoadState('already_reviewed'); return }
if (!r.ok) { setLoadState('invalid'); return }
const json = await r.json()
setInfo(json.data)
setLoadState('ready')
})
.catch(() => setLoadState('invalid'))
}, [token])
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (overall === 0) { setErrorMsg('Please select an overall rating.'); return }
setSubmitState('submitting')
setErrorMsg('')
try {
const res = await fetch(`${API_BASE}/marketplace/review/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
overallRating: overall,
vehicleRating: vehicle || undefined,
serviceRating: service || undefined,
comment: comment.trim() || undefined,
}),
})
if (res.status === 409) { setLoadState('already_reviewed'); return }
if (!res.ok) {
const json = await res.json()
throw new Error(json.message ?? 'Something went wrong.')
}
setSubmitState('done')
} catch (err: any) {
setSubmitState('error')
setErrorMsg(err.message ?? 'Something went wrong. Please try again.')
}
}
if (loadState === 'loading') {
return (
<main className="flex min-h-screen items-center justify-center bg-stone-50 p-6">
<p className="text-stone-500 text-sm">Loading</p>
</main>
)
}
if (loadState === 'invalid') {
return (
<main className="flex min-h-screen items-center justify-center bg-stone-50 p-6">
<div className="card max-w-md w-full p-8 text-center space-y-3">
<p className="text-2xl font-black text-stone-900">Link not found</p>
<p className="text-stone-500 text-sm">This review link is invalid or has already been used.</p>
</div>
</main>
)
}
if (loadState === 'already_reviewed') {
return (
<main className="flex min-h-screen items-center justify-center bg-stone-50 p-6">
<div className="card max-w-md w-full p-8 text-center space-y-3">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-emerald-100">
<svg className="h-7 w-7 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
</div>
<p className="text-xl font-bold text-stone-900">Already reviewed</p>
<p className="text-stone-500 text-sm">You have already submitted a review for this rental. Thank you!</p>
</div>
</main>
)
}
if (submitState === 'done') {
return (
<main className="flex min-h-screen items-center justify-center bg-stone-50 p-6">
<div className="card max-w-md w-full p-8 text-center space-y-4">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100">
<svg className="h-8 w-8 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
</div>
<h1 className="text-2xl font-black text-stone-900">Thank you!</h1>
<p className="text-stone-500">Your review has been submitted and will be visible on {info?.companyName}'s profile.</p>
</div>
</main>
)
}
return (
<main className="flex min-h-screen items-center justify-center bg-stone-50 p-6">
<div className="card max-w-lg w-full overflow-hidden">
{/* Header */}
<div className="flex items-center gap-4 border-b border-stone-100 p-6">
{info?.vehiclePhoto ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={info.vehiclePhoto} alt={info.vehicle} className="h-16 w-24 rounded-xl object-cover flex-shrink-0" />
) : (
<div className="h-16 w-24 rounded-xl bg-stone-100 flex-shrink-0" />
)}
<div className="min-w-0">
<p className="text-xs uppercase tracking-widest text-stone-400 truncate">{info?.companyName}</p>
<h1 className="mt-0.5 text-lg font-bold text-stone-900 truncate">{info?.vehicle}</h1>
<p className="mt-0.5 text-sm text-stone-500">How was your experience?</p>
</div>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<StarRating value={overall} onChange={setOverall} label="Overall experience *" />
<StarRating value={vehicle} onChange={setVehicle} label="Vehicle condition (optional)" />
<StarRating value={service} onChange={setService} label="Customer service (optional)" />
<div>
<label className="block text-sm font-medium text-stone-700 mb-1.5">Comments (optional)</label>
<textarea
rows={4}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Tell us about your rental experience…"
className="w-full rounded-xl border border-stone-200 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-stone-900 resize-none"
/>
</div>
{errorMsg && (
<p className="rounded-xl bg-rose-50 px-4 py-3 text-sm text-rose-700">{errorMsg}</p>
)}
<button
type="submit"
disabled={submitState === 'submitting'}
className="w-full rounded-full bg-stone-900 py-3 text-sm font-semibold text-white transition-colors hover:bg-stone-700 disabled:opacity-60"
>
{submitState === 'submitting' ? 'Submitting' : 'Submit review'}
</button>
</form>
</div>
</main>
)
}
@@ -121,10 +121,12 @@ export default function MarketplaceShell({ children }: { children: React.ReactNo
const router = useRouter()
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001')
const adminUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002')
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
const [language, setLanguage] = useState<MarketplaceLanguage>('en')
const [theme, setTheme] = useState<Theme>('light')
const [hydrated, setHydrated] = useState(false)
const previousLanguage = useRef<MarketplaceLanguage>('en')
const [companyName, setCompanyName] = useState<string | null>(null)
useEffect(() => {
const storedLanguage = window.localStorage.getItem('marketplace-language')
@@ -139,6 +141,19 @@ export default function MarketplaceShell({ children }: { children: React.ReactNo
}
setHydrated(true)
const token = document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('employee_token='))?.split('=')[1]
if (token) {
fetch(`${apiUrl}/companies/me/brand`, {
headers: { Authorization: `Bearer ${token}` },
})
.then(r => r.ok ? r.json() : null)
.then(json => {
const name = json?.data?.displayName ?? json?.displayName
if (name) setCompanyName(name)
})
.catch(() => {})
}
}, [])
useEffect(() => {
@@ -212,12 +227,24 @@ export default function MarketplaceShell({ children }: { children: React.ReactNo
>
{dict.explore}
</Link>
<Link
href={`${dashboardUrl}/sign-in`}
className="ml-2 rounded-full bg-stone-900 px-5 py-2 text-sm font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
>
{dict.ownerSignIn}
</Link>
{companyName ? (
<Link
href={`${dashboardUrl}/dashboard`}
className="ml-2 flex items-center gap-2 rounded-full bg-stone-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-[10px] font-black dark:bg-stone-950/20">
{companyName.charAt(0).toUpperCase()}
</span>
{companyName}
</Link>
) : (
<Link
href={`${dashboardUrl}/sign-in`}
className="ml-2 rounded-full bg-stone-900 px-5 py-2 text-sm font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
>
{dict.ownerSignIn}
</Link>
)}
</nav>
</div>
</div>