"use client"; import { useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { ScanBarcode, Camera, Loader2, X, ArrowLeft } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { BarcodeScanner } from "@/components/pantry/barcode-scanner"; type Step = "choose" | "barcode-scan" | "barcode-confirm" | "photo-loading" | "photo-confirm"; type DraftItem = { rawName: string; quantity: string; unit: string }; function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result as string; const comma = result.indexOf(","); resolve(comma !== -1 ? result.slice(comma + 1) : result); }; reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); } export function PantryScanDialog({ onAdded }: { onAdded: () => void }) { const t = useTranslations("pantry"); const [open, setOpen] = useState(false); const [step, setStep] = useState("choose"); const [barcodeDraft, setBarcodeDraft] = useState(null); const [photoDrafts, setPhotoDrafts] = useState([]); const [busy, setBusy] = useState(false); const fileRef = useRef(null); function reset() { setStep("choose"); setBarcodeDraft(null); setPhotoDrafts([]); } function handleOpenChange(next: boolean) { setOpen(next); if (!next) reset(); } async function handleBarcodeDetected(barcode: string) { setBusy(true); try { const res = await fetch("/api/v1/pantry/scan/barcode", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ barcode }), }); if (!res.ok) { toast.error(t("scanLookupFailed")); setStep("choose"); return; } const data = await res.json() as { found: boolean; rawName?: string; quantity?: string; unit?: string }; if (!data.found || !data.rawName) { toast.error(t("scanNotFound")); setStep("choose"); return; } setBarcodeDraft({ rawName: data.rawName, quantity: data.quantity ?? "", unit: data.unit ?? "" }); setStep("barcode-confirm"); } finally { setBusy(false); } } async function handlePhotoFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setStep("photo-loading"); try { const imageBase64 = await fileToBase64(file); const mimeType = (["image/jpeg", "image/png", "image/webp"].includes(file.type) ? file.type : "image/jpeg") as | "image/jpeg" | "image/png" | "image/webp"; const res = await fetch("/api/v1/pantry/scan/photo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageBase64, mimeType }), }); if (!res.ok) { const data = await res.json().catch(() => ({})) as { error?: string }; toast.error(data.error ?? t("scanPhotoFailed")); setStep("choose"); return; } const data = await res.json() as { items: { rawName: string; estimatedQuantity?: string; unit?: string }[] }; if (data.items.length === 0) { toast.error(t("scanNoItemsFound")); setStep("choose"); return; } setPhotoDrafts( data.items.map((i) => ({ rawName: i.rawName, quantity: i.estimatedQuantity ?? "", unit: i.unit ?? "" })) ); setStep("photo-confirm"); } finally { if (fileRef.current) fileRef.current.value = ""; } } async function confirmItems(items: DraftItem[]) { setBusy(true); try { const res = await fetch("/api/v1/pantry/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: items .filter((i) => i.rawName.trim()) .map((i) => ({ rawName: i.rawName.trim(), quantity: i.quantity.trim() || undefined, unit: i.unit.trim() || undefined, })), }), }); if (!res.ok) { toast.error(t("addFailed")); return; } toast.success(t("scanAdded")); onAdded(); handleOpenChange(false); } finally { setBusy(false); } } return ( <> {step === "choose" && t("scanTitle")} {step === "barcode-scan" && t("scanBarcodeTitle")} {step === "barcode-confirm" && t("scanConfirmTitle")} {step === "photo-loading" && t("scanAnalyzingTitle")} {step === "photo-confirm" && t("scanConfirmTitle")} {step === "choose" && t("scanDescription")} {step === "barcode-scan" && t("scanBarcodeDescription")} {(step === "barcode-confirm" || step === "photo-confirm") && t("scanConfirmDescription")} {step === "choose" && (
)} {step === "barcode-scan" && (
void handleBarcodeDetected(barcode)} onError={() => { toast.error(t("cameraError")); setStep("choose"); }} />
)} {step === "barcode-confirm" && barcodeDraft && (
setBarcodeDraft({ ...barcodeDraft, rawName: e.target.value })} placeholder={t("itemNamePlaceholder")} className="flex-1" /> setBarcodeDraft({ ...barcodeDraft, quantity: e.target.value })} placeholder={t("qtyPlaceholder")} className="w-20" /> setBarcodeDraft({ ...barcodeDraft, unit: e.target.value })} placeholder={t("unitPlaceholder")} className="w-20" />
)} {step === "photo-loading" && (

{t("scanAnalyzingDescription")}

)} {step === "photo-confirm" && (
{photoDrafts.map((draft, i) => (
setPhotoDrafts((prev) => prev.map((d, idx) => (idx === i ? { ...d, rawName: e.target.value } : d))) } className="flex-1" /> setPhotoDrafts((prev) => prev.map((d, idx) => (idx === i ? { ...d, quantity: e.target.value } : d))) } placeholder={t("qtyPlaceholder")} className="w-16" /> setPhotoDrafts((prev) => prev.map((d, idx) => (idx === i ? { ...d, unit: e.target.value } : d))) } placeholder={t("unitPlaceholder")} className="w-16" />
))}
)} {(step === "barcode-confirm" || step === "photo-confirm") && ( )}
); }