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

586 lines
18 KiB
TypeScript
Raw Normal View History

2026-01-07 08:06:07 +00:00
"use client";
2026-01-28 18:29:25 +00:00
import { useState, useEffect } from "react";
2026-01-07 08:06:07 +00:00
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";
2026-01-28 18:29:25 +00:00
import { useParams } from "next/navigation";
import { getProductDataById, updateProduct } from "@/service/product";
import { useRouter } from "next/navigation";
import { success, error, close, loading } from "@/config/swal";
2026-01-07 08:06:07 +00:00
export default function UpdateProductForm() {
2026-01-28 18:29:25 +00:00
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());
2026-01-07 08:06:07 +00:00
type ColorType = {
id: number;
name: string;
preview: string;
colorSelected: string | null;
};
2026-01-28 18:29:25 +00:00
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>("");
2026-01-07 08:06:07 +00:00
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: [],
2026-01-28 18:29:25 +00:00
files: [],
2026-01-07 08:06:07 +00:00
},
]);
};
const handleAddColor = () => {
setColors((p) => [
...p,
{
id: p.length + 1,
name: "",
preview: "/car-default.png",
colorSelected: null,
},
]);
};
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
2026-01-28 18:29:25 +00:00
const [bannerFile, setBannerFile] = useState<File | null>(null);
2026-01-07 08:06:07 +00:00
const [uploadTarget, setUploadTarget] = useState<{
2026-01-28 18:29:25 +00:00
type: "spec" | "color" | "banner";
index?: number;
2026-01-07 08:06:07 +00:00
} | null>(null);
const fileInputId = "file-upload-input";
2026-01-28 18:29:25 +00:00
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target;
const file = input.files?.[0];
2026-01-07 08:06:07 +00:00
if (!file || !uploadTarget) return;
2026-01-28 18:29:25 +00:00
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);
};
2026-01-07 08:06:07 +00:00
2026-01-28 18:29:25 +00:00
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([]);
2026-01-07 08:06:07 +00:00
}
2026-01-28 18:29:25 +00:00
// 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);
2026-01-07 08:06:07 +00:00
}
2026-01-28 18:29:25 +00:00
// 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);
}
2026-01-07 08:06:07 +00:00
};
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>
2026-01-28 18:29:25 +00:00
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Masukkan nama produk"
/>
2026-01-07 08:06:07 +00:00
</div>
<div>
<Label>Tipe Varian *</Label>
2026-01-28 18:29:25 +00:00
<Input
value={variant}
onChange={(e) => setVariant(e.target.value)}
placeholder="Masukkan varian"
/>
2026-01-07 08:06:07 +00:00
</div>
<div>
<Label>Harga Produk *</Label>
2026-01-28 18:29:25 +00:00
<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>
2026-01-07 08:06:07 +00:00
</div>
</div>
<div>
<Label className="font-semibold">Warna Produk *</Label>
{colors.map((item, index) => (
2026-01-26 03:04:14 +00:00
<div
key={item.id}
className="mt-6 pb-6 border-2 rounded-lg border-black p-3"
>
2026-01-07 08:06:07 +00:00
<Label>Pilih Warna {index + 1}</Label>
<Input
placeholder="Contoh: Silver / #E2E2E2"
className="mt-1"
2026-01-28 18:29:25 +00:00
value={item.name}
onChange={(e) => {
setColors((prev) => {
const updated = [...prev];
updated[index].name = e.target.value;
updated[index].colorSelected = e.target.value;
return updated;
});
}}
2026-01-07 08:06:07 +00:00
/>
<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;
2026-01-28 18:29:25 +00:00
updated[index].name = colorCode; // ✅ sinkron ke input
2026-01-07 08:06:07 +00:00
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
2026-01-28 18:29:25 +00:00
value={spec.title}
2026-01-07 08:06:07 +00:00
placeholder="Masukkan Judul Spesifikasi"
className="mt-1"
2026-01-28 18:29:25 +00:00
onChange={(e) => {
setSpecs((prev) => {
const updated = [...prev];
updated[index].title = e.target.value;
return updated;
});
}}
2026-01-07 08:06:07 +00:00
/>
<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) => (
2026-01-28 18:29:25 +00:00
<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>
2026-01-07 08:06:07 +00:00
))}
<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>
2026-01-28 18:29:25 +00:00
<Button
onClick={handleSubmit}
className=" bg-teal-800 hover:bg-teal-900 text-white py-3"
>
2026-01-07 08:06:07 +00:00
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>
</>
);
}