"use client"; import { useRef, useState } from "react"; import Image from "next/image"; import { ImagePlus, X, Loader2 } from "lucide-react"; import { LIMITS } from "@/lib/limits"; interface TripImageUploadProps { /** URL gambar yang sudah terunggah (path `/api/trip-images/...`). */ value: string[]; onChange: (urls: string[]) => void; /** Lapor error ke form (mis. file terlalu besar / gagal upload). */ onError?: (msg: string) => void; } const ACCEPT_MIME = "image/jpeg,image/png,image/webp"; /** Sinkron dengan MAX_TRIP_IMAGE_UPLOAD_BYTES di lib/trip-image-storage.ts. */ const MAX_BYTES = 12 * 1024 * 1024; /** * Pengganti input URL foto: user memilih file dari perangkatnya, tiap file * langsung di-upload & dikompres server-side. Form hanya menyimpan URL hasil. */ export function TripImageUpload({ value, onChange, onError, }: TripImageUploadProps) { const inputRef = useRef(null); const [uploadingCount, setUploadingCount] = useState(0); const max = LIMITS.MAX_IMAGE_URLS; const usedSlots = value.length + uploadingCount; const remaining = max - usedSlots; async function uploadOne(file: File): Promise { if (!ACCEPT_MIME.split(",").includes(file.type)) { onError?.(`"${file.name}" harus JPG, PNG, atau WebP`); return null; } if (file.size > MAX_BYTES) { onError?.(`"${file.name}" melebihi 12MB`); return null; } const fd = new FormData(); fd.set("file", file); try { const res = await fetch("/api/upload/trip-image", { method: "POST", body: fd, }); const json = await res.json(); if (!res.ok) { onError?.(json.error ?? `Gagal mengunggah "${file.name}"`); return null; } return json.url as string; } catch { onError?.(`Gagal mengunggah "${file.name}"`); return null; } } async function handlePick(e: React.ChangeEvent) { const files = Array.from(e.target.files ?? []); e.target.value = ""; if (files.length === 0) return; if (remaining <= 0) { onError?.(`Maksimal ${max} foto`); return; } const picked = files.slice(0, remaining); if (files.length > remaining) { onError?.(`Hanya ${remaining} foto pertama diunggah (maks ${max})`); } setUploadingCount((c) => c + picked.length); // `value` di-snapshot saat handler dibuat; upload sekuensial supaya urutan // foto stabil, lalu hasil yang berhasil ditambahkan sekali di akhir. const uploaded: string[] = []; for (const file of picked) { const url = await uploadOne(file); setUploadingCount((c) => c - 1); if (url) uploaded.push(url); } if (uploaded.length > 0) onChange([...value, ...uploaded]); } function removeAt(index: number) { onChange(value.filter((_, i) => i !== index)); } return (
{value.map((url, i) => (
{i {i === 0 && ( Cover )}
))} {Array.from({ length: uploadingCount }).map((_, i) => (
))} {remaining > 0 && ( )}

Unggah langsung dari galeri/kamera — JPG, PNG, atau WebP, maks 12MB per foto. Foto pertama jadi cover. Gambar besar otomatis dikompres tanpa mengorbankan kualitas.

); }