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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user