add review and booking policies

This commit is contained in:
root
2026-05-25 18:29:05 -04:00
parent 8ed572b3bd
commit 0d969ab095
37 changed files with 3775 additions and 113 deletions
@@ -0,0 +1,124 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { apiFetch } from '@/lib/api'
interface ReservationPhoto {
id: string
type: 'PICKUP' | 'DROPOFF'
url: string
createdAt: string
}
interface Props {
reservationId: string
type: 'PICKUP' | 'DROPOFF'
editable: boolean
title: string
readOnlyMessage?: string
}
export default function ReservationPhotoSection({ reservationId, type, editable, title, readOnlyMessage }: Props) {
const [photos, setPhotos] = useState<ReservationPhoto[]>([])
const [uploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const loadPhotos = useCallback(async () => {
try {
const all = await apiFetch<ReservationPhoto[]>(`/reservations/${reservationId}/photos`)
setPhotos(all.filter((p) => p.type === type))
} catch {
// silent — don't block the page if photos fail to load
}
}, [reservationId, type])
useEffect(() => { loadPhotos() }, [loadPhotos])
async function handleFiles(files: FileList | null) {
if (!files || files.length === 0) return
setUploading(true)
setError(null)
try {
for (const file of Array.from(files)) {
const body = new FormData()
body.append('photo', file)
body.append('type', type)
await apiFetch(`/reservations/${reservationId}/photos`, { method: 'POST', body })
}
await loadPhotos()
} catch (err: any) {
setError(err.message ?? 'Upload failed')
} finally {
setUploading(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
async function handleDelete(photoId: string) {
setError(null)
try {
await apiFetch(`/reservations/${reservationId}/photos/${photoId}`, { method: 'DELETE' })
setPhotos((prev) => prev.filter((p) => p.id !== photoId))
} catch (err: any) {
setError(err.message ?? 'Delete failed')
}
}
return (
<div className="card p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
{editable && (
<button
type="button"
disabled={uploading}
onClick={() => fileInputRef.current?.click()}
className="btn-secondary text-xs"
>
{uploading ? 'Uploading…' : '+ Add photos'}
</button>
)}
</div>
{!editable && readOnlyMessage && (
<p className="mb-4 text-xs text-slate-500">{readOnlyMessage}</p>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => handleFiles(e.target.files)}
/>
{error && <p className="mb-3 text-xs text-red-600">{error}</p>}
{photos.length === 0 ? (
<p className="text-sm text-slate-400">No photos yet.</p>
) : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
{photos.map((photo) => (
<div key={photo.id} className="group relative aspect-square overflow-hidden rounded-xl border border-slate-200">
<img src={photo.url} alt="" className="h-full w-full object-cover" />
{editable && (
<button
type="button"
onClick={() => handleDelete(photo.id)}
className="absolute right-1 top-1 hidden h-6 w-6 items-center justify-center rounded-full bg-black/60 text-white group-hover:flex"
aria-label="Delete photo"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="h-3 w-3">
<path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" />
</svg>
</button>
)}
</div>
))}
</div>
)}
</div>
)
}