fix and update language for company workspace
This commit is contained in:
@@ -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: 'البريد الإلكتروني',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user