add review and booking policies
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user