595 lines
18 KiB
TypeScript
595 lines
18 KiB
TypeScript
"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;
|
||
isImageChanged: boolean;
|
||
};
|
||
|
||
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 = [
|
||
"#000000",
|
||
"#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,
|
||
isImageChanged: false, // ✅ WAJIB
|
||
},
|
||
]);
|
||
};
|
||
|
||
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;
|
||
updated[index].isImageChanged = true; // 🔥 PENTING
|
||
return updated;
|
||
});
|
||
|
||
setColorFiles((prev) => {
|
||
const map = new Map(prev);
|
||
map.set(index, file);
|
||
return map;
|
||
});
|
||
}
|
||
|
||
// ===================== 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 || "",
|
||
colorSelected: color.name || null,
|
||
isImageChanged: false, // 🔥 default
|
||
})),
|
||
);
|
||
} 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));
|
||
|
||
colors.forEach((color, index) => {
|
||
if (color.isImageChanged) {
|
||
// image diganti → kirim file baru
|
||
const file = colorFiles.get(index);
|
||
if (file) {
|
||
formData.append("color_images", file);
|
||
} else {
|
||
formData.append("color_images", new Blob([])); // safety
|
||
}
|
||
} else {
|
||
// image tidak diganti → kirim placeholder
|
||
formData.append("color_images", new Blob([]));
|
||
}
|
||
});
|
||
|
||
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"
|
||
/>
|
||
|
||
<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>
|
||
</>
|
||
);
|
||
}
|