jaecoo-cihampelas/components/form/product/update-product-form.tsx

586 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { Upload, Plus, Settings } from "lucide-react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useParams } from "next/navigation";
import { getProductDataById, updateProduct } from "@/service/product";
import { useRouter } from "next/navigation";
import { success, error, close, loading } from "@/config/swal";
export default function UpdateProductForm() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const [specs, setSpecs] = useState<
{ id: number; title: string; images: string[]; files: File[] }[]
>([]);
const [specFiles, setSpecFiles] = useState<Map<number, File[]>>(new Map());
type ColorType = {
id: number;
name: string;
preview: string;
colorSelected: string | null;
};
const [colors, setColors] = useState<ColorType[]>([]);
const [colorFiles, setColorFiles] = useState<Map<number, File>>(new Map());
const [thumbnail, setThumbnail] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [variant, setVariant] = useState<string>("");
const [price, setPrice] = useState<string>("");
const palette = [
"#1E4E52",
"#597E8D",
"#6B6B6B",
"#BEBEBE",
"#E2E2E2",
"#F4F4F4",
"#FFFFFF",
"#F9360A",
"#9A2A00",
"#7A1400",
"#4B0200",
"#B48B84",
"#FFA598",
];
const handleAddSpec = () => {
setSpecs((prev) => [
...prev,
{
id: prev.length + 1,
title: "",
images: [],
files: [],
},
]);
};
const handleAddColor = () => {
setColors((p) => [
...p,
{
id: p.length + 1,
name: "",
preview: "/car-default.png",
colorSelected: null,
},
]);
};
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [bannerFile, setBannerFile] = useState<File | null>(null);
const [uploadTarget, setUploadTarget] = useState<{
type: "spec" | "color" | "banner";
index?: number;
} | null>(null);
const fileInputId = "file-upload-input";
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target;
const file = input.files?.[0];
if (!file || !uploadTarget) return;
const previewUrl = URL.createObjectURL(file);
// ===================== SPEC =====================
if (uploadTarget.type === "spec") {
if (uploadTarget.index === undefined) return;
const index = uploadTarget.index;
// preview
setSpecs((prev) => {
const updated = [...prev];
updated[index].images = [...updated[index].images, previewUrl];
return updated;
});
// file upload
setSpecFiles((prev) => {
const newMap = new Map(prev);
const files = newMap.get(index) || [];
newMap.set(index, [...files, file]);
return newMap;
});
}
// ===================== COLOR =====================
if (uploadTarget.type === "color") {
if (uploadTarget.index === undefined) return;
const index = uploadTarget.index;
setColors((prev) => {
const updated = [...prev];
updated[index].preview = previewUrl;
return updated;
});
setColorFiles((prev) => {
const newMap = new Map(prev);
newMap.set(index, file);
return newMap;
});
}
// ===================== BANNER =====================
if (uploadTarget.type === "banner") {
setThumbnail(previewUrl);
setBannerFile(file);
}
input.value = "";
setIsUploadDialogOpen(false);
};
const formatRupiah = (value: string) =>
"Rp " + Number(value).toLocaleString("id-ID");
useEffect(() => {
if (id) {
initState();
}
}, [id]);
async function initState() {
if (!id) return;
try {
loading();
const res = await getProductDataById(id);
const data = res?.data?.data;
if (!data) {
error("Produk tidak ditemukan");
return;
}
close();
// Set form values
setTitle(data.title || "");
setVariant(data.variant || "");
setPrice(formatRupiah(data.price || "0"));
setThumbnail(data.thumbnail_url || "");
// Set colors
if (data.colors?.length) {
setColors(
data.colors.map((color: any, index: number) => ({
id: index + 1,
name: color.name || "",
preview: color.image_url || data.thumbnail_url || "",
colorSelected: color.name || null,
})),
);
} else {
setColors([]);
}
// Set specifications
if (data.specifications?.length) {
setSpecs(
data.specifications.map((spec: any, index: number) => ({
id: index + 1,
title: spec.title || "",
images: spec.image_urls || [],
files: [],
})),
);
} else {
setSpecs([]);
}
} catch (err) {
error("Gagal memuat data produk");
console.error(err);
}
}
const handleSubmit = async () => {
if (!id) {
error("ID produk tidak ditemukan");
return;
}
try {
loading();
const formData = new FormData();
formData.append("title", title);
if (variant) formData.append("variant", variant);
if (price) {
const priceValue = price.replace(/\D/g, "");
formData.append("price", priceValue);
}
// Colors JSON
const colorsPayload = colors.map((c) => ({
name: c.name,
}));
formData.append("colors", JSON.stringify(colorsPayload));
// Color images (only new files if uploaded)
// Append files in order of color indices
colors.forEach((_, index) => {
const file = colorFiles.get(index);
if (file) {
formData.append("color_images", file);
}
});
if (bannerFile) {
formData.append("file", bannerFile);
}
// Specifications JSON (include image count for new files only)
const specificationsPayload = specs.map((s, index) => {
const newFiles = specFiles.get(index) || [];
return {
title: s.title,
imageCount: newFiles.length, // Only count new files being uploaded
};
});
formData.append("specifications", JSON.stringify(specificationsPayload));
specs.forEach((_, index) => {
const files = specFiles.get(index) || [];
files.forEach((file) => {
formData.append("specification_images", file);
});
});
await updateProduct(formData, id);
success("Produk berhasil diperbarui");
router.push("/admin/product");
} catch (err) {
error("Gagal memperbarui produk");
console.error(err);
}
};
return (
<>
<Card className="w-full border-none shadow-md">
<CardHeader>
<CardTitle className="text-xl font-bold text-teal-900">
Edit Produk
</CardTitle>
</CardHeader>
<CardContent className="space-y-8">
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label>Nama Produk *</Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Masukkan nama produk"
/>
</div>
<div>
<Label>Tipe Varian *</Label>
<Input
value={variant}
onChange={(e) => setVariant(e.target.value)}
placeholder="Masukkan varian"
/>
</div>
<div>
<Label>Harga Produk *</Label>
<Input
value={price}
onChange={(e) => {
const rawValue = e.target.value.replace(/\D/g, "");
setPrice(formatRupiah(rawValue));
}}
placeholder="Masukkan harga"
/>
</div>
</div>
<div>
<Label className="font-semibold">Thumbnail Produk</Label>
<div className="mt-2 flex items-center gap-4">
<div className="w-[120px] h-[80px] rounded-lg overflow-hidden border bg-gray-100">
{thumbnail ? (
<Image
src={thumbnail}
alt="Thumbnail"
width={120}
height={80}
className="object-cover"
/>
) : (
<div className="flex items-center justify-center text-xs text-gray-400 h-full">
No Image
</div>
)}
</div>
<Button
type="button"
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "banner" });
setIsUploadDialogOpen(true);
}}
>
Upload Banner Baru
</Button>
</div>
</div>
<div>
<Label className="font-semibold">Warna Produk *</Label>
{colors.map((item, index) => (
<div
key={item.id}
className="mt-6 pb-6 border-2 rounded-lg border-black p-3"
>
<Label>Pilih Warna {index + 1}</Label>
<Input
placeholder="Contoh: Silver / #E2E2E2"
className="mt-1"
value={item.name}
onChange={(e) => {
setColors((prev) => {
const updated = [...prev];
updated[index].name = e.target.value;
updated[index].colorSelected = e.target.value;
return updated;
});
}}
/>
<div className="flex items-center gap-2 mt-3">
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-teal-900">
<Settings className="w-5 h-5 text-white" />
</div>
{palette.map((colorCode) => (
<button
key={colorCode}
type="button"
style={{ backgroundColor: colorCode }}
className={`w-10 h-10 rounded-full border-2 transition ${
item.colorSelected === colorCode
? "border-teal-700 scale-110"
: "border-gray-300"
}`}
onClick={() => {
setColors((prev) => {
const updated = [...prev];
updated[index].colorSelected = colorCode;
updated[index].name = colorCode; // ✅ sinkron ke input
return updated;
});
}}
/>
))}
</div>
<div className="mt-4">
<Label className="font-semibold">
Foto Produk Warna {index + 1}
</Label>
<div className="flex items-center gap-4 mt-2">
<Image
src={item.preview}
alt="car color"
width={120}
height={80}
className="object-cover rounded-md border"
/>
<Button
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "color", index });
setIsUploadDialogOpen(true);
}}
>
Upload File Baru
</Button>
</div>
</div>
</div>
))}
<Button
type="button"
onClick={handleAddColor}
className="w-full bg-teal-800 hover:bg-teal-900 text-white mt-4"
>
<Plus className="w-4 h-4 mr-2" /> Tambah Warna Baru
</Button>
</div>
<div className="mt-10">
<Label className="text-lg font-semibold text-teal-900">
Spesifikasi Produk <span className="text-red-500">*</span>
</Label>
{specs.map((spec, index) => (
<div key={spec.id} className="mt-6">
<Label className="font-semibold text-sm">
Judul Spesifikasi {index + 1}
</Label>
<Input
value={spec.title}
placeholder="Masukkan Judul Spesifikasi"
className="mt-1"
onChange={(e) => {
setSpecs((prev) => {
const updated = [...prev];
updated[index].title = e.target.value;
return updated;
});
}}
/>
<Label className="font-semibold text-sm mt-4 block">
Foto Spesifikasi {index + 1}
</Label>
<div className="flex flex-wrap gap-4 mt-2">
{spec.images.map((img, i) => (
<div key={i} className="relative">
<Image
src={img}
width={120}
height={120}
alt="spec"
className="rounded-lg border object-cover"
/>
{/* 🔴 TOMBOL HAPUS GAMBAR */}
<button
type="button"
onClick={() => {
// hapus preview
setSpecs((prev) => {
const updated = [...prev];
updated[index].images.splice(i, 1);
return updated;
});
// hapus file baru (jika ada)
setSpecFiles((prev) => {
const map = new Map(prev);
const files = map.get(index) || [];
files.splice(i, 1);
map.set(index, files);
return map;
});
}}
className="absolute -top-2 -right-2 bg-red-500 text-white
rounded-full w-6 h-6 flex items-center justify-center
text-xs hover:bg-red-600"
>
×
</button>
</div>
))}
<Button
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "spec", index });
setIsUploadDialogOpen(true);
}}
>
Upload File Baru
</Button>
</div>
<div className="my-6 border-b"></div>
</div>
))}
<Button
onClick={handleAddSpec}
className="w-full bg-teal-800 hover:bg-teal-900 text-white flex items-center justify-center gap-2 py-4 rounded-xl"
>
<Plus className="w-4 h-4" />
Tambahkan Spesifikasi Baru
</Button>
</div>
<Button
onClick={handleSubmit}
className=" bg-teal-800 hover:bg-teal-900 text-white py-3"
>
Submit
</Button>
</CardContent>
</Card>
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-teal-900 font-semibold">
Upload File
</DialogTitle>
</DialogHeader>
<div
className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer"
onClick={() => document.getElementById(fileInputId)?.click()}
>
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<p className="text-sm text-gray-500">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p>
<input
id={fileInputId}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelected}
/>
</div>
<DialogFooter className="flex gap-3 pt-4">
<Button
variant="secondary"
className="bg-slate-200"
onClick={() => setIsUploadDialogOpen(false)}
>
Batal
</Button>
<Button
onClick={() => document.getElementById(fileInputId)?.click()}
className="bg-teal-800 hover:bg-teal-900 text-white"
>
Pilih File
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}