Compare commits
10 Commits
ae03db15be
...
0d7286e65a
| Author | SHA1 | Date |
|---|---|---|
|
|
0d7286e65a | |
|
|
05225a3319 | |
|
|
df8de78666 | |
|
|
9c01e6f467 | |
|
|
fbc021f3b4 | |
|
|
9c3b082c7e | |
|
|
a9b4aab886 | |
|
|
7d3ac22960 | |
|
|
01df3f41d4 | |
|
|
83eae371a6 |
|
|
@ -0,0 +1,10 @@
|
|||
import UpdateProductForm from "@/components/form/product/update-product-form";
|
||||
|
||||
export default function UpdateProductPage() {
|
||||
return (
|
||||
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
|
||||
<UpdateProductForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import ExteriorShs from "@/components/landing-page/exterior-shs";
|
||||
import FeaturesAndSpecificationsShs from "@/components/landing-page/features-and-specifications-shs";
|
||||
import Footer from "@/components/landing-page/footer";
|
||||
import HeaderProductJ5Ev from "@/components/landing-page/header-product-j7-shs";
|
||||
import HeaderProductJ7Shs from "@/components/landing-page/header-product-j7-shs";
|
||||
import InteriorShs from "@/components/landing-page/interior-shs";
|
||||
import Navbar from "@/components/landing-page/navbar";
|
||||
|
|
@ -10,7 +11,7 @@ export default function ProductJ7ShsPage() {
|
|||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="relative z-10 bg-white w-full mx-auto">
|
||||
<Navbar />
|
||||
<HeaderProductJ7Shs />
|
||||
<HeaderProductJ5Ev />
|
||||
<ExteriorShs />
|
||||
<InteriorShs />
|
||||
<FeaturesAndSpecificationsShs />
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
const res = await commentGalery(id, message || undefined);
|
||||
|
||||
if (res?.error) {
|
||||
error(res.message || "Gagal komentar promotion");
|
||||
error(res.message || "Gagal komentar galeri");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -161,11 +161,24 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className=" rounded-2xl p-0 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-[#1F6779] text-white px-6 py-4">
|
||||
<DialogTitle className="text-white">Detail Galeri</DialogTitle>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-2xl max-w-xl w-full overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h2 className="text-lg font-semibold">Detail Galeri</h2>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<span
|
||||
className={`text-xs font-medium px-3 py-1 rounded-full ${
|
||||
|
|
@ -199,7 +212,7 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
|
||||
<div className=" py-3">
|
||||
{/* Images List */}
|
||||
<div className="mx-2">
|
||||
<div className="mx-4">
|
||||
<h2 className="text-2xl font-semibold text-black">
|
||||
{data.title}
|
||||
</h2>
|
||||
|
|
@ -240,7 +253,7 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
{/* Deskripsi */}
|
||||
|
||||
{/* Tanggal Upload */}
|
||||
<div className="mx-2">
|
||||
<div className="mx-4">
|
||||
<p className="font-medium text-sm text-gray-700">
|
||||
Tanggal Upload
|
||||
</p>
|
||||
|
|
@ -250,7 +263,7 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="mx-2">
|
||||
<div className="mx-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">
|
||||
Status Timeline
|
||||
</h4>
|
||||
|
|
@ -374,17 +387,8 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <DialogFooter className="px-6 pb-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</DialogFooter> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
{openApproverHistory && (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,22 @@ import { Upload, X } from "lucide-react";
|
|||
import { useState, useRef, useEffect } from "react";
|
||||
import { createGalery, uploadGaleryFile } from "@/service/galery";
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
"Grand Opening",
|
||||
"IIMS",
|
||||
"GIIAS",
|
||||
"GJAW",
|
||||
"Exhibitions",
|
||||
"Test Drive",
|
||||
];
|
||||
|
||||
export function GaleriDialog({ open, onClose, onSubmit }: any) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [previews, setPreviews] = useState<string[]>([]);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [category, setCategory] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!files || files.length === 0) {
|
||||
|
|
@ -47,10 +57,11 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
|
|||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!title) return alert("Judul wajib diisi!");
|
||||
|
||||
if (!category) return alert("Category wajib diisi!");
|
||||
const formData = new FormData();
|
||||
formData.append("title", title);
|
||||
formData.append("description", description);
|
||||
formData.append("category", category);
|
||||
|
||||
const res = await createGalery(formData);
|
||||
|
||||
|
|
@ -73,7 +84,7 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
|
|||
}
|
||||
|
||||
onSubmit();
|
||||
|
||||
setCategory("");
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setFiles([]);
|
||||
|
|
@ -106,6 +117,26 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<label className="font-medium text-sm">
|
||||
Category <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="mt-1 h-12 w-full rounded-md border border-gray-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#1F6779]"
|
||||
>
|
||||
<option value="">Pilih category</option>
|
||||
{CATEGORY_OPTIONS.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<div>
|
||||
<label className="font-medium text-sm">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { error, loading, success } from "@/config/swal";
|
|||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
import promo from "../landing-page/promo";
|
||||
import Image from "next/image";
|
||||
|
||||
type PromoDetailDialogProps = {
|
||||
promoId: number | null;
|
||||
|
|
@ -220,16 +221,24 @@ export default function PromoDetailDialog({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="p-0 max-w-lg overflow-hidden">
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-2xl max-w-xl w-full overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white text-xl font-semibold">
|
||||
Detail Promoaa
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h2 className="text-lg font-semibold">Detail Promo</h2>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<span
|
||||
className={`text-xs font-medium px-3 py-1 rounded-full ${
|
||||
|
|
@ -279,8 +288,15 @@ export default function PromoDetailDialog({
|
|||
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<p className="text-gray-600">Ukuran File</p>
|
||||
<p className="font-medium">{promo.fileSize}</p>
|
||||
<div className="w-24 h-24 rounded-lg overflow-hidden border">
|
||||
<img
|
||||
src={promo.thumbnail_url}
|
||||
alt="Profile"
|
||||
width={96}
|
||||
height={96}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -418,104 +434,6 @@ export default function PromoDetailDialog({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{openApproverHistory && (
|
||||
<div
|
||||
className=" flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={() => setOpenApproverHistory(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
||||
<button
|
||||
onClick={() => setOpenApproverHistory(false)}
|
||||
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h2 className="text-lg font-semibold">Approver History</h2>
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
|
||||
Menunggu
|
||||
</span>
|
||||
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
|
||||
Banner
|
||||
</span>
|
||||
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="p-6 grid grid-cols-[1fr_auto_1fr] gap-6 items-start">
|
||||
{/* LEFT TIMELINE */}
|
||||
<div className="relative space-y-6">
|
||||
{/* Upload */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="bg-[#C7DDE4] text-[#0F6C75] text-xs px-4 py-1 rounded-full">
|
||||
Upload
|
||||
</span>
|
||||
<div className="w-px h-6 bg-gray-300" />
|
||||
</div>
|
||||
|
||||
{/* Diterima */}
|
||||
<div className="relative bg-[#1F6779] text-white rounded-xl p-4">
|
||||
<h4 className="font-semibold text-sm mb-2">Diterima</h4>
|
||||
<span className="inline-block bg-[#E3EFF4] text-[#0F6C75] text-xs px-3 py-1 rounded-full">
|
||||
Direview oleh: approver-jaecoo1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-px h-6 bg-gray-300 mx-auto" />
|
||||
|
||||
{/* Pending */}
|
||||
<div className="relative bg-[#B36A00] text-white rounded-xl p-4">
|
||||
<h4 className="font-semibold text-sm mb-2">Pending</h4>
|
||||
<span className="inline-block bg-[#FFF6CC] text-[#7A4A00] text-xs px-3 py-1 rounded-full">
|
||||
Direview oleh: approver-jaecoo1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ARROW */}
|
||||
<div className="flex flex-col gap-20 text-gray-500 font-bold">
|
||||
<span>></span>
|
||||
<span>></span>
|
||||
</div>
|
||||
|
||||
{/* RIGHT NOTES */}
|
||||
<div className="space-y-14">
|
||||
<div>
|
||||
<div className="bg-[#C7DDE4] text-sm px-4 py-2 rounded-lg">
|
||||
Catatan:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="bg-[#FFF9C4] text-sm px-4 py-2 rounded-lg">
|
||||
Catatan:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="border-t bg-[#F2F7FA] text-center py-3">
|
||||
<button
|
||||
onClick={() => setOpenApproverHistory(false)}
|
||||
className="text-[#0F6C75] font-medium hover:underline"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* FOOTER
|
||||
<div className="bg-[#E3EFF4] text-center py-3">
|
||||
<button
|
||||
|
|
@ -525,8 +443,8 @@ export default function PromoDetailDialog({
|
|||
Tutup
|
||||
</button>
|
||||
</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
{openApproverHistory && (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Cookies from "js-cookie";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
import { getPromotionById, updatePromotion } from "@/service/promotion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { loading, success, error } from "@/config/swal";
|
||||
|
||||
type PromoEditDialogProps = {
|
||||
promoId: number | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export default function PromoEditDialog({
|
||||
promoId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}: PromoEditDialogProps) {
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
// 🔹 FORM STATE
|
||||
const [title, setTitle] = useState("");
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
/* =========================
|
||||
* Fetch promo detail
|
||||
========================= */
|
||||
useEffect(() => {
|
||||
if (!promoId || !open) return;
|
||||
|
||||
async function fetchPromo() {
|
||||
try {
|
||||
setLoadingData(true);
|
||||
const res = await getPromotionById(promoId);
|
||||
const data = res?.data?.data;
|
||||
|
||||
setTitle(data?.title || "");
|
||||
setThumbnailUrl(data?.thumbnail_url || null);
|
||||
} catch (e) {
|
||||
console.error("FETCH PROMO ERROR:", e);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchPromo();
|
||||
}, [promoId, open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
/* =========================
|
||||
* Handlers
|
||||
========================= */
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setThumbnailFile(file);
|
||||
setThumbnailUrl(URL.createObjectURL(file));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!promoId) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("title", title);
|
||||
|
||||
if (thumbnailFile) {
|
||||
formData.append("file", thumbnailFile);
|
||||
}
|
||||
|
||||
loading();
|
||||
const res = await updatePromotion(formData, promoId);
|
||||
|
||||
if (res?.error) {
|
||||
error(res.message || "Gagal update promo");
|
||||
return;
|
||||
}
|
||||
|
||||
success("Promo berhasil diperbarui");
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
};
|
||||
|
||||
/* =========================
|
||||
* Render
|
||||
========================= */
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-xl max-w-lg w-full overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="absolute top-4 right-4 text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold">Edit Promo</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="p-6 space-y-5">
|
||||
{loadingData ? (
|
||||
<p>Memuat data...</p>
|
||||
) : (
|
||||
<>
|
||||
{/* TITLE */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||
Judul Promo
|
||||
</label>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-[#0F6C75]"
|
||||
placeholder="Judul promo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* THUMBNAIL */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600 mb-2">
|
||||
Thumbnail
|
||||
</label>
|
||||
|
||||
{thumbnailUrl && (
|
||||
<div className="w-32 h-32 mb-3 rounded-lg overflow-hidden border">
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt="Thumbnail"
|
||||
className="w-32 h-32 object-cover rounded-lg border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="bg-[#0F6C75] text-white rounded-full px-2 w-[230px]"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="flex justify-end gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="bg-[#1F6779] hover:bg-[#0F6C75] text-white"
|
||||
onClick={handleSubmit}
|
||||
disabled={loadingData}
|
||||
>
|
||||
Simpan Perubahan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -278,7 +278,7 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
|
|||
<div>
|
||||
<Label className="mb-2 block">Foto Profile</Label>
|
||||
<div className="w-24 h-24 rounded-lg overflow-hidden border">
|
||||
<Image
|
||||
<img
|
||||
src={data?.profile_picture_url}
|
||||
alt="Profile"
|
||||
width={96}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
|
|
@ -8,6 +8,14 @@ import * as z from "zod";
|
|||
import { Upload, Plus, UploadCloud } from "lucide-react";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Label } from "@radix-ui/react-dropdown-menu";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -15,6 +23,7 @@ import { createProduct } from "@/service/product";
|
|||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loading } from "@/config/swal";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Nama produk wajib diisi"),
|
||||
|
|
@ -24,9 +33,25 @@ const formSchema = z.object({
|
|||
});
|
||||
|
||||
export default function AddProductForm() {
|
||||
const [colors, setColors] = useState([{ id: 1 }]);
|
||||
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||
const [specs, setSpecs] = useState([{ id: 1 }]);
|
||||
const [colors, setColors] = useState<
|
||||
{ id: number; name: string; file: File | null }[]
|
||||
>([{ id: 1, name: "", file: null }]);
|
||||
|
||||
// const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||
const [specs, setSpecs] = useState<
|
||||
{ id: number; title: string; images: string[]; files: File[] }[]
|
||||
>([{ id: 1, title: "", images: [], files: [] }]);
|
||||
|
||||
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
||||
const [uploadTarget, setUploadTarget] = useState<{
|
||||
type: "spec";
|
||||
index: number;
|
||||
} | null>(null);
|
||||
|
||||
const fileInputId = "spec-upload-input";
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const isUploadingRef = useRef(false);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const router = useRouter();
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -38,7 +63,10 @@ export default function AddProductForm() {
|
|||
};
|
||||
|
||||
const handleAddSpec = () => {
|
||||
setSpecs((prev) => [...prev, { id: prev.length + 1 }]);
|
||||
setSpecs((prev) => [
|
||||
...prev,
|
||||
{ id: prev.length + 1, title: "", images: [], files: [] },
|
||||
]);
|
||||
};
|
||||
const {
|
||||
register,
|
||||
|
|
@ -49,29 +77,107 @@ export default function AddProductForm() {
|
|||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const handleSpecTitleChange = (index: number, value: string) => {
|
||||
const updated = [...specs];
|
||||
updated[index].title = value;
|
||||
setSpecs(updated);
|
||||
};
|
||||
|
||||
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target;
|
||||
const selectedFile = input.files?.[0];
|
||||
|
||||
if (!selectedFile || !uploadTarget) return;
|
||||
|
||||
// 🔒 CEGAH DOUBLE EVENT
|
||||
if (isUploadingRef.current) return;
|
||||
isUploadingRef.current = true;
|
||||
|
||||
setSpecs((prev) => {
|
||||
const updated = [...prev];
|
||||
const spec = updated[uploadTarget.index];
|
||||
|
||||
// max 5 gambar
|
||||
if (spec.files.length >= 5) {
|
||||
isUploadingRef.current = false;
|
||||
return prev;
|
||||
}
|
||||
|
||||
// cegah file sama
|
||||
if (
|
||||
spec.files.some(
|
||||
(f) => f.name === selectedFile.name && f.size === selectedFile.size,
|
||||
)
|
||||
) {
|
||||
isUploadingRef.current = false;
|
||||
return prev;
|
||||
}
|
||||
|
||||
const previewUrl = URL.createObjectURL(selectedFile);
|
||||
|
||||
spec.images = [...spec.images, previewUrl];
|
||||
spec.files = [...spec.files, selectedFile];
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
// reset input
|
||||
input.value = "";
|
||||
setIsUploadDialogOpen(false);
|
||||
|
||||
// release lock
|
||||
setTimeout(() => {
|
||||
isUploadingRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
loading();
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("title", data.name);
|
||||
formData.append("variant", data.variant);
|
||||
formData.append("is_active", "1");
|
||||
formData.append("price", data.price.toString());
|
||||
// if (data.banner && data.banner.length > 0) {
|
||||
// formData.append("thumbnail_path", data.banner[0]);
|
||||
// }
|
||||
formData.append("status", "1");
|
||||
formData.append("is_active", "1");
|
||||
|
||||
// banner
|
||||
if (file) {
|
||||
formData.append("file", file);
|
||||
}
|
||||
const colorsArray = colors.map((c) => selectedColor || "");
|
||||
formData.append("colors", JSON.stringify(colorsArray));
|
||||
|
||||
const res = await createProduct(formData);
|
||||
// 🔥 colors JSON (object)
|
||||
const colorsPayload = colors.map((c) => ({
|
||||
name: c.name,
|
||||
}));
|
||||
formData.append("colors", JSON.stringify(colorsPayload));
|
||||
|
||||
console.log("API Success:", res);
|
||||
// 🔥 color images
|
||||
colors.forEach((c) => {
|
||||
if (c.file) {
|
||||
formData.append("color_images", c.file);
|
||||
}
|
||||
});
|
||||
|
||||
// 🔥 specifications JSON (include image count for backend processing)
|
||||
const specificationsPayload = specs.map((s) => ({
|
||||
title: s.title,
|
||||
imageCount: s.files.length,
|
||||
}));
|
||||
formData.append("specifications", JSON.stringify(specificationsPayload));
|
||||
|
||||
// 🔥 specification images (multiple files per spec)
|
||||
specs.forEach((s) => {
|
||||
s.files.forEach((file) => {
|
||||
formData.append("specification_images", file);
|
||||
});
|
||||
});
|
||||
|
||||
await createProduct(formData);
|
||||
successSubmit("/admin/product");
|
||||
} catch (error) {
|
||||
console.error("Submit Error:", error);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Gagal mengirim produk");
|
||||
}
|
||||
};
|
||||
|
|
@ -96,7 +202,20 @@ export default function AddProductForm() {
|
|||
};
|
||||
|
||||
const handleAddColor = () => {
|
||||
setColors((prev) => [...prev, { id: prev.length + 1 }]);
|
||||
setColors((prev) => [
|
||||
...prev,
|
||||
{ id: prev.length + 1, name: "", file: null },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleColorFileChange = (
|
||||
index: number,
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = e.target.files?.[0] || null;
|
||||
const updated = [...colors];
|
||||
updated[index].file = file;
|
||||
setColors(updated);
|
||||
};
|
||||
|
||||
const formatRupiah = (value: string) => {
|
||||
|
|
@ -115,7 +234,9 @@ export default function AddProductForm() {
|
|||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Nama Produk *</Label>
|
||||
<Label>
|
||||
Nama Produk <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Masukkan Nama Produk" {...register("name")} />
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-500 mt-1">
|
||||
|
|
@ -125,7 +246,9 @@ export default function AddProductForm() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tipe Varian *</Label>
|
||||
<Label>
|
||||
Tipe Varian <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Contoh: AWD, SHS, EV"
|
||||
{...register("variant")}
|
||||
|
|
@ -138,7 +261,9 @@ export default function AddProductForm() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Harga Produk *</Label>
|
||||
<Label>
|
||||
Harga Produk <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Masukkan Harga Produk"
|
||||
value={priceDisplay}
|
||||
|
|
@ -183,7 +308,9 @@ export default function AddProductForm() {
|
|||
|
||||
{/* Upload Produk */}
|
||||
<div>
|
||||
<Label>Upload Produk *</Label>
|
||||
<Label>
|
||||
Upload Produk <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
{colors.map((color, index) => (
|
||||
<div
|
||||
key={color.id}
|
||||
|
|
@ -192,7 +319,14 @@ export default function AddProductForm() {
|
|||
<Label className="text-sm font-semibold">
|
||||
Pilih Warna {index + 1}
|
||||
</Label>
|
||||
<Input placeholder="Contoh: Silver or #E2E2E2" />
|
||||
<Input
|
||||
value={color.name}
|
||||
onChange={(e) => {
|
||||
const updated = [...colors];
|
||||
updated[index].name = e.target.value;
|
||||
setColors(updated);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pilihan Warna */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -214,12 +348,18 @@ export default function AddProductForm() {
|
|||
<button
|
||||
key={colorCode}
|
||||
type="button"
|
||||
onClick={() => setSelectedColor(colorCode)}
|
||||
className={`w-8 h-8 rounded-full border-2 transition ${
|
||||
selectedColor === colorCode
|
||||
? "border-teal-700 scale-110"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
const updated = [...colors];
|
||||
updated[index].name = colorCode;
|
||||
setColors(updated);
|
||||
}}
|
||||
className={`w-8 h-8 rounded-full border-2 transition
|
||||
${
|
||||
colors[index].name === colorCode
|
||||
? "border-teal-700 scale-110"
|
||||
: "border-gray-200"
|
||||
}
|
||||
`}
|
||||
style={{ backgroundColor: colorCode }}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -227,19 +367,29 @@ export default function AddProductForm() {
|
|||
|
||||
{/* Upload Foto Warna */}
|
||||
<div>
|
||||
<Label>Foto Warna {index + 1}</Label>
|
||||
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-6 cursor-pointer hover:bg-gray-50 transition">
|
||||
<label
|
||||
htmlFor={`color-file-${index}`}
|
||||
className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-6 cursor-pointer hover:bg-gray-50 transition"
|
||||
>
|
||||
<Upload className="w-6 h-6 text-gray-400 mb-2" />
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
Klik untuk upload foto mobil warna ini
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
|
||||
|
||||
<input
|
||||
id={`color-file-${index}`}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg"
|
||||
className="hidden"
|
||||
onChange={(e) => handleColorFileChange(index, e)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
{color.file && (
|
||||
<p className="text-xs text-teal-700 mt-2">
|
||||
{color.file.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -259,41 +409,76 @@ export default function AddProductForm() {
|
|||
</Label>
|
||||
|
||||
{specs.map((spec, index) => (
|
||||
<div key={spec.id} className="mt-4">
|
||||
<div
|
||||
key={spec.id}
|
||||
className="mt-6 border-2 rounded-lg border-black p-3"
|
||||
>
|
||||
<Label className="text-sm font-semibold">
|
||||
Judul Spesifikasi {index + 1}
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Contoh: Mesin Turbo 1.6L, Interior Premium, Safety Features"
|
||||
placeholder="Contoh: Mesin Turbo 1.6L"
|
||||
className="mt-1"
|
||||
value={spec.title}
|
||||
onChange={(e) => handleSpecTitleChange(index, e.target.value)}
|
||||
/>
|
||||
|
||||
<Label className="text-sm font-semibold mt-4 block">
|
||||
Foto Spesifikasi {index + 1}
|
||||
</Label>
|
||||
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-10 cursor-pointer hover:bg-gray-50 transition mt-1">
|
||||
<Upload className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
Klik untuk upload gambar spesifikasi
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg"
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<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={() => {
|
||||
setSpecs((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[index].images.splice(i, 1);
|
||||
updated[index].files.splice(i, 1);
|
||||
return updated;
|
||||
});
|
||||
}}
|
||||
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
|
||||
type="button"
|
||||
className="bg-teal-800 hover:bg-teal-900 text-white"
|
||||
onClick={() => {
|
||||
setUploadTarget({ type: "spec", index });
|
||||
setIsUploadDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Tambah Foto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="my-4 border-b"></div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddSpec}
|
||||
className="w-full bg-teal-800 hover:bg-teal-900 text-white py-3 rounded-lg mt-6 flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Tambahkan Spesifikasi Baru
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
|
@ -304,6 +489,48 @@ export default function AddProductForm() {
|
|||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<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">
|
||||
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500">
|
||||
Klik tombol di bawah untuk memilih file
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFileSelected}
|
||||
/>
|
||||
|
||||
<DialogFooter className="flex gap-3 pt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsUploadDialogOpen(false)}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="bg-teal-800 hover:bg-teal-900 text-white"
|
||||
>
|
||||
Pilih File
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,13 +159,13 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
|
|||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
const [specs, setSpecs] = useState([
|
||||
const [specs, setSpecs] = useState<
|
||||
{
|
||||
id: 1,
|
||||
title: "Jaecoo 7 SHS Teknologi dan Exterior",
|
||||
images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"],
|
||||
},
|
||||
]);
|
||||
id: number;
|
||||
title: string;
|
||||
images: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
type ColorType = {
|
||||
id: number;
|
||||
|
|
@ -174,20 +174,7 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
|
|||
colorSelected: string | null;
|
||||
};
|
||||
|
||||
const [colors, setColors] = useState<ColorType[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "",
|
||||
preview: "/car-1.png",
|
||||
colorSelected: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "",
|
||||
preview: "/car-2.png",
|
||||
colorSelected: null,
|
||||
},
|
||||
]);
|
||||
const [colors, setColors] = useState<ColorType[]>([]);
|
||||
|
||||
const palette = [
|
||||
"#1E4E52",
|
||||
|
|
@ -297,13 +284,31 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
|
|||
setThumbnail(data.thumbnail_url);
|
||||
|
||||
// colors
|
||||
if (data.colors?.length) {
|
||||
if (Array.isArray(data.colors)) {
|
||||
setColors(
|
||||
data.colors.map((color: string, index: number) => ({
|
||||
data.colors.map((color: any, index: number) => ({
|
||||
id: color.id ?? index + 1,
|
||||
name: color.name ?? "",
|
||||
preview:
|
||||
typeof color.image_url === "string" && color.image_url.length > 0
|
||||
? color.image_url
|
||||
: "/car-default.png", // fallback aman
|
||||
colorSelected: color.name ?? null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// specifications
|
||||
if (Array.isArray(data.specifications)) {
|
||||
setSpecs(
|
||||
data.specifications.map((spec: any, index: number) => ({
|
||||
id: index + 1,
|
||||
name: color,
|
||||
preview: data.thumbnail_url,
|
||||
colorSelected: color,
|
||||
title: spec.title ?? "",
|
||||
images: Array.isArray(spec.image_urls)
|
||||
? spec.image_urls.filter(
|
||||
(url: any) => typeof url === "string" && url.length > 0,
|
||||
)
|
||||
: [],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
|
@ -509,36 +514,44 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
|
|||
<div>
|
||||
<Label className="font-semibold text-gray-700">Warna Produk</Label>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-4 gap-6 border-2 rounded-lg border-black p-3 ">
|
||||
{colors.map((item, index) => (
|
||||
<div key={item.id} className="space-y-3">
|
||||
<Label className="text-sm text-gray-500">
|
||||
Warna {index + 1}
|
||||
</Label>
|
||||
{colors.length === 0 ? (
|
||||
<p className="mt-3 text-sm text-gray-400 italic">
|
||||
Tidak ada data warna
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-4 gap-6 border-2 rounded-lg border-black p-3">
|
||||
{colors.map((item, index) => (
|
||||
<div key={item.id} className="space-y-3">
|
||||
<Label className="text-sm text-gray-500">
|
||||
Warna {index + 1}
|
||||
</Label>
|
||||
|
||||
{/* Preview warna */}
|
||||
<div
|
||||
className="w-12 h-12 rounded-full border"
|
||||
style={{ backgroundColor: item.colorSelected ?? "#e5e7eb" }}
|
||||
/>
|
||||
|
||||
{/* Foto mobil */}
|
||||
<div className="w-full h-[90px] border rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={item.preview}
|
||||
alt="warna"
|
||||
width={200}
|
||||
height={120}
|
||||
className="object-cover w-full h-full"
|
||||
{/* Preview warna */}
|
||||
<div
|
||||
className="w-12 h-12 rounded-full border"
|
||||
style={{
|
||||
backgroundColor: item.colorSelected || "#e5e7eb",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Foto Produk Warna {index + 1}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Foto mobil */}
|
||||
<div className="w-full h-[90px] border rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={item.preview}
|
||||
alt={`warna-${index}`}
|
||||
width={200}
|
||||
height={120}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Foto Produk Warna {index + 1}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -546,29 +559,45 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
|
|||
Spesifikasi Produk
|
||||
</Label>
|
||||
|
||||
{specs.map((spec) => (
|
||||
<div
|
||||
key={spec.id}
|
||||
className="mt-4 space-y-3 border-2 rounded-lg border-black p-3"
|
||||
>
|
||||
<Input value={spec.title} readOnly className="font-medium" />
|
||||
{specs.length === 0 ? (
|
||||
<p className="mt-3 text-sm text-gray-400 italic">
|
||||
Tidak ada spesifikasi
|
||||
</p>
|
||||
) : (
|
||||
specs.map((spec) => (
|
||||
<div
|
||||
key={spec.id}
|
||||
className="mt-4 space-y-3 border-2 rounded-lg border-black p-3"
|
||||
>
|
||||
<Input value={spec.title} readOnly className="font-medium" />
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{spec.images.map((img, i) => (
|
||||
<div key={i} className="border rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={img}
|
||||
alt="spec"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-cover w-full h-[120px]"
|
||||
/>
|
||||
{spec.images.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 italic">
|
||||
Tidak ada gambar spesifikasi
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{spec.images.map((img, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border rounded-lg overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt={`spec-${i}`}
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-cover w-full h-[120px]"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">
|
||||
Status Timeline
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Upload, Plus, Settings } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -14,39 +14,37 @@ import {
|
|||
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 [specs, setSpecs] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: "Jaecoo 7 SHS Teknologi dan Exterior",
|
||||
images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"],
|
||||
},
|
||||
]);
|
||||
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[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "",
|
||||
preview: "/car-1.png",
|
||||
colorSelected: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "",
|
||||
preview: "/car-2.png",
|
||||
colorSelected: 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 = [
|
||||
"#000000",
|
||||
"#1E4E52",
|
||||
"#597E8D",
|
||||
"#6B6B6B",
|
||||
|
|
@ -69,6 +67,7 @@ export default function UpdateProductForm() {
|
|||
id: prev.length + 1,
|
||||
title: "",
|
||||
images: [],
|
||||
files: [],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
|
@ -81,47 +80,210 @@ export default function UpdateProductForm() {
|
|||
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";
|
||||
index: number;
|
||||
type: "spec" | "color" | "banner";
|
||||
index?: number;
|
||||
} | null>(null);
|
||||
|
||||
const fileInputId = "file-upload-input";
|
||||
|
||||
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target;
|
||||
const file = input.files?.[0];
|
||||
if (!file || !uploadTarget) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const fileUrl = reader.result as string;
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
|
||||
if (uploadTarget.type === "spec") {
|
||||
setSpecs((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[uploadTarget.index].images.push(fileUrl);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
// ===================== SPEC =====================
|
||||
if (uploadTarget.type === "spec") {
|
||||
if (uploadTarget.index === undefined) return;
|
||||
|
||||
if (uploadTarget.type === "color") {
|
||||
setColors((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[uploadTarget.index].preview = fileUrl;
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
const index = uploadTarget.index;
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
// 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">
|
||||
|
|
@ -135,17 +297,64 @@ export default function UpdateProductForm() {
|
|||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>Nama Produk *</Label>
|
||||
<Input defaultValue="JAECOO J7" />
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Masukkan nama produk"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Tipe Varian *</Label>
|
||||
<Input defaultValue="SHS" />
|
||||
<Input
|
||||
value={variant}
|
||||
onChange={(e) => setVariant(e.target.value)}
|
||||
placeholder="Masukkan varian"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Harga Produk *</Label>
|
||||
<Input defaultValue="RP 599.000.000" />
|
||||
<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>
|
||||
|
||||
|
|
@ -161,7 +370,15 @@ export default function UpdateProductForm() {
|
|||
<Input
|
||||
placeholder="Contoh: Silver / #E2E2E2"
|
||||
className="mt-1"
|
||||
defaultValue={item.name}
|
||||
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">
|
||||
|
|
@ -183,6 +400,7 @@ export default function UpdateProductForm() {
|
|||
setColors((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[index].colorSelected = colorCode;
|
||||
updated[index].name = colorCode; // ✅ sinkron ke input
|
||||
return updated;
|
||||
});
|
||||
}}
|
||||
|
|
@ -238,9 +456,16 @@ export default function UpdateProductForm() {
|
|||
Judul Spesifikasi {index + 1}
|
||||
</Label>
|
||||
<Input
|
||||
defaultValue={spec.title}
|
||||
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">
|
||||
|
|
@ -249,14 +474,41 @@ export default function UpdateProductForm() {
|
|||
|
||||
<div className="flex flex-wrap gap-4 mt-2">
|
||||
{spec.images.map((img, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
src={img}
|
||||
width={120}
|
||||
height={120}
|
||||
alt="spec"
|
||||
className="rounded-lg border object-cover"
|
||||
/>
|
||||
<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
|
||||
|
|
@ -283,7 +535,10 @@ export default function UpdateProductForm() {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<Button className=" bg-teal-800 hover:bg-teal-900 text-white py-3">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className=" bg-teal-800 hover:bg-teal-900 text-white py-3"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -2,28 +2,54 @@
|
|||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAgentData } from "@/service/agent";
|
||||
|
||||
const agents = [
|
||||
{
|
||||
name: "Johny Nugroho",
|
||||
title: "Branch Manager Jaecoo Cihampelas Bandung",
|
||||
image: "/johny.png",
|
||||
},
|
||||
{
|
||||
name: "Basuki Pamungkas",
|
||||
title: "Spv Jaecoo Cihampelas Bandung",
|
||||
image: "/basuki.png",
|
||||
},
|
||||
{
|
||||
name: "Deni Tihayar",
|
||||
title: "Spv Jaecoo Cihampelas Bandung",
|
||||
image: "/deni.png",
|
||||
},
|
||||
];
|
||||
type Agent = {
|
||||
id: number;
|
||||
name: string;
|
||||
job_title: string;
|
||||
status_id: number;
|
||||
profile_picture_url: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export default function Agent() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
const req = {
|
||||
limit: "10",
|
||||
page: 1,
|
||||
search: "",
|
||||
};
|
||||
|
||||
const res = await getAgentData(req);
|
||||
|
||||
const agentsData: Agent[] = res?.data?.data || [];
|
||||
|
||||
const latestApprovedAgents = agentsData
|
||||
.filter((agent) => agent.status_id === 2) // ✅ approved only
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() -
|
||||
new Date(a.created_at).getTime(),
|
||||
) // ✅ newest first
|
||||
.slice(0, 3); // ✅ max 5
|
||||
|
||||
setAgents(latestApprovedAgents);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch agents:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgents();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="py-16 px-6 md:px-12 bg-[#f9f9f9] text-center mt-0">
|
||||
<section className="py-16 px-6 md:px-5 bg-[#FAFDFF] text-center mt-0">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -34,20 +60,10 @@ export default function Agent() {
|
|||
Our Teams
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-gray-600 mb-10 text-lg"
|
||||
>
|
||||
Temui anggota tim kami yang luar biasa
|
||||
</motion.p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 place-items-center">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 place-items-center mt-10">
|
||||
{agents.map((agent, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
|
|
@ -56,19 +72,21 @@ export default function Agent() {
|
|||
ease: "easeOut",
|
||||
}}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="bg-white shadow-md px-2 py-4 gap-4 flex flex-col items-center h-[300px] w-[224px]"
|
||||
className="bg-white shadow-md py-4 gap-2 flex flex-col items-center h-[300px] w-[250px]"
|
||||
>
|
||||
<div className="relative w-28 h-36 mb-3">
|
||||
<div className="relative w-44 h-48 mb-3">
|
||||
<Image
|
||||
src={agent.image}
|
||||
src={agent.profile_picture_url}
|
||||
alt={agent.name}
|
||||
fill
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3>
|
||||
<p className="text-xs text-gray-600 text-center mt-1">
|
||||
{agent.title}
|
||||
|
||||
<p className="text-sm text-gray-600 text-center mt-1">
|
||||
{agent.job_title}Cihampelas Bandung
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -2,42 +2,68 @@
|
|||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAgentData } from "@/service/agent";
|
||||
|
||||
const agents = [
|
||||
{
|
||||
name: "Johny Nugroho",
|
||||
title: "Branch Manager Jaecoo Cihampelas Bandung",
|
||||
image: "/johny.png",
|
||||
},
|
||||
{
|
||||
name: "Basuki Pamungkas",
|
||||
title: "Spv Jaecoo Cihampelas Bandung",
|
||||
image: "/basuki.png",
|
||||
},
|
||||
{
|
||||
name: "Deni Tihayar",
|
||||
title: "Spv Jaecoo Cihampelas Bandung",
|
||||
image: "/deni.png",
|
||||
},
|
||||
];
|
||||
type Agent = {
|
||||
id: number;
|
||||
name: string;
|
||||
job_title: string;
|
||||
status_id: number;
|
||||
profile_picture_url: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export default function BestAgent() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
const req = {
|
||||
limit: "10",
|
||||
page: 1,
|
||||
search: "",
|
||||
};
|
||||
|
||||
const res = await getAgentData(req);
|
||||
|
||||
const agentsData: Agent[] = res?.data?.data || [];
|
||||
|
||||
const latestApprovedAgents = agentsData
|
||||
.filter((agent) => agent.status_id === 2) // ✅ approved only
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() -
|
||||
new Date(a.created_at).getTime(),
|
||||
) // ✅ newest first
|
||||
.slice(0, 3); // ✅ max 5
|
||||
|
||||
setAgents(latestApprovedAgents);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch agents:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgents();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="py-16 px-4 sm:px-6 md:px-12 bg-[#f9f9f9] text-center mt-0">
|
||||
<section className="py-16 px-6 md:px-5 bg-[#FAFDFF] text-center mt-0">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-2xl sm:text-3xl md:text-4xl font-semibold text-gray-900 mb-10"
|
||||
className="text-3xl md:text-6xl font-semibold text-gray-900 mb-2"
|
||||
>
|
||||
Our Teams
|
||||
</motion.h2>
|
||||
|
||||
<div className="flex flex-col md:flex-row flex-wrap items-center justify-center gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 place-items-center mt-10">
|
||||
{agents.map((agent, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
|
|
@ -46,19 +72,21 @@ export default function BestAgent() {
|
|||
ease: "easeOut",
|
||||
}}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="bg-white shadow-md p-4 flex flex-col items-center w-full max-w-[224px] h-[340px] sm:h-[300px]"
|
||||
className="bg-white shadow-md py-4 gap-2 flex flex-col items-center h-[300px] w-[250px]"
|
||||
>
|
||||
<div className="relative w-28 h-36 mb-3">
|
||||
<div className="relative w-44 h-48 mb-3">
|
||||
<Image
|
||||
src={agent.image}
|
||||
src={agent.profile_picture_url}
|
||||
alt={agent.name}
|
||||
fill
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3>
|
||||
<p className="text-xs text-gray-600 text-center mt-1">
|
||||
{agent.title}
|
||||
|
||||
<p className="text-sm text-gray-600 text-center mt-1">
|
||||
{agent.job_title}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
|
|
@ -6,79 +7,57 @@ export default function Footer() {
|
|||
<div className="flex flex-col md:flex-row gap-10">
|
||||
<div className="w-full md:w-4/12">
|
||||
<Image
|
||||
src="/masjaecoo.png"
|
||||
src="/jaecoobot.png"
|
||||
alt="Jaecoo"
|
||||
width={300}
|
||||
height={200}
|
||||
className="ml-4"
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 mt-6 ml-24 md:ml-20 text-xl text-[#c7dbe3]">
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
// stroke-width="1.5"
|
||||
<div className="flex gap-4 mt-6 ml-8 text-xl text-[#c7dbe3]">
|
||||
<Link href={"https://www.instagram.com/jaecoocihampelasbdg"}>
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
// stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
// stroke-linecap="round"
|
||||
// stroke-linejoin="round"
|
||||
d="M12 16a4 4 0 1 0 0-8a4 4 0 0 0 0 8"
|
||||
/>
|
||||
<path d="M3 16V8a5 5 0 0 1 5-5h8a5 5 0 0 1 5 5v8a5 5 0 0 1-5 5H8a5 5 0 0 1-5-5Z" />
|
||||
<path
|
||||
// stroke-linecap="round"
|
||||
// stroke-linejoin="round"
|
||||
d="m17.5 6.51l.01-.011"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={"https://www.tiktok.com/@jaecoo.cihampelasbdg"}>
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
// stroke-linecap="round"
|
||||
// stroke-linejoin="round"
|
||||
d="M12 16a4 4 0 1 0 0-8a4 4 0 0 0 0 8"
|
||||
fill="currentColor"
|
||||
d="M16 8.245V15.5a6.5 6.5 0 1 1-5-6.326v3.163a3.5 3.5 0 1 0 2 3.163V2h3a5 5 0 0 0 5 5v3a7.97 7.97 0 0 1-5-1.755"
|
||||
/>
|
||||
<path d="M3 16V8a5 5 0 0 1 5-5h8a5 5 0 0 1 5 5v8a5 5 0 0 1-5 5H8a5 5 0 0 1-5-5Z" />
|
||||
<path
|
||||
// stroke-linecap="round"
|
||||
// stroke-linejoin="round"
|
||||
d="m17.5 6.51l.01-.011"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 8.245V15.5a6.5 6.5 0 1 1-5-6.326v3.163a3.5 3.5 0 1 0 2 3.163V2h3a5 5 0 0 0 5 5v3a7.97 7.97 0 0 1-5-1.755"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="hover:text-white cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.244 4c.534.003 1.87.016 3.29.073l.504.022c1.429.067 2.857.183 3.566.38c.945.266 1.687 1.04 1.938 2.022c.4 1.56.45 4.602.456 5.339l.001.152v.174c-.007.737-.057 3.78-.457 5.339c-.254.985-.997 1.76-1.938 2.022c-.709.197-2.137.313-3.566.38l-.504.023c-1.42.056-2.756.07-3.29.072l-.235.001h-.255c-1.13-.007-5.856-.058-7.36-.476c-.944-.266-1.687-1.04-1.938-2.022c-.4-1.56-.45-4.602-.456-5.339v-.326c.006-.737.056-3.78.456-5.339c.254-.985.997-1.76 1.939-2.021c1.503-.419 6.23-.47 7.36-.476zM9.999 8.5v7l6-3.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:w-8/12 ">
|
||||
|
|
@ -130,10 +109,14 @@ export default function Footer() {
|
|||
<h4 className="font-semibold text-white mb-4">CONTACT</h4>
|
||||
<ul className="space-y-4 text-sm">
|
||||
<li>
|
||||
<a href="https://jaecoo.com" target="_blank" rel="noreferrer">
|
||||
jaecoo.com
|
||||
<a
|
||||
href="mailto:jaecoocihampelas@gmail.com"
|
||||
className="hover:underline"
|
||||
>
|
||||
jaecoocihampelas@gmail.com
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>0817 222190</li>
|
||||
<li>
|
||||
<p className="font-semibold text-white">Jaecoo Bandung</p>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,127 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
const imagesPerPage = 6;
|
||||
|
||||
const galleryImages = [
|
||||
"/gl1.png",
|
||||
"/gl2-new.png",
|
||||
"/gl3.png",
|
||||
"/gl4.png",
|
||||
"/gl5.png",
|
||||
"/gl6.png",
|
||||
"/gl7.png",
|
||||
"/gl8.png",
|
||||
"/gl9.png",
|
||||
];
|
||||
import {
|
||||
getAllGaleryFiles,
|
||||
getGaleryData,
|
||||
getGaleryFileData,
|
||||
} from "@/service/galery";
|
||||
|
||||
export default function GallerySection() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = Math.ceil(galleryImages.length / imagesPerPage);
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const TABS = [
|
||||
"All",
|
||||
"Grand Opening",
|
||||
"IIMS",
|
||||
"GIIAS",
|
||||
"GJAW",
|
||||
"Exhibitions",
|
||||
"Test Drive",
|
||||
];
|
||||
|
||||
const paginatedImages = galleryImages.slice(
|
||||
(currentPage - 1) * imagesPerPage,
|
||||
currentPage * imagesPerPage
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState("All");
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 1️⃣ Ambil gallery (ada status_id)
|
||||
const galleryRes = await getGaleryData({
|
||||
limit: "100",
|
||||
page: 1,
|
||||
search: "",
|
||||
});
|
||||
|
||||
const galleries = galleryRes?.data?.data ?? [];
|
||||
|
||||
// hanya approved
|
||||
const approvedGalleries = galleries.filter((g: any) => g.status_id === 2);
|
||||
|
||||
// 2️⃣ Ambil SEMUA files
|
||||
const filesRes = await getAllGaleryFiles();
|
||||
const files = filesRes?.data?.data ?? [];
|
||||
|
||||
// 3️⃣ Mapping gallery + file berdasarkan gallery_id
|
||||
const merged = approvedGalleries.map((gallery: any) => {
|
||||
const file = files.find((f: any) => f.gallery_id === gallery.id);
|
||||
|
||||
return {
|
||||
...gallery,
|
||||
image_url: file?.image_url ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
setData(merged);
|
||||
} catch (err) {
|
||||
console.error("Error fetch galeri:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const filteredData = data.filter((item) => {
|
||||
if (activeTab === "All") return true;
|
||||
return item.category === activeTab;
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="py-16 px-4 max-w-[1400px] mx-auto">
|
||||
<h2 className="text-4xl font-bold mb-8">Galeri Kami</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{paginatedImages.map((img, index) => (
|
||||
<div key={index} className="relative w-full aspect-[3/2]">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`gallery-${index}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 mt-10">
|
||||
<button
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 rounded-md hover:bg-gray-200 disabled:opacity-30"
|
||||
>
|
||||
<ChevronLeft />
|
||||
</button>
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
<div className="flex justify-center gap-2 mb-10">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`w-8 h-8 rounded-md border text-sm ${
|
||||
currentPage === i + 1
|
||||
? "bg-[#1F6779] text-white"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition
|
||||
${
|
||||
activeTab === tab
|
||||
? "bg-black text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-2 rounded-md hover:bg-gray-200 disabled:opacity-30"
|
||||
>
|
||||
<ChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-center">Loading...</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item: any) => (
|
||||
<div
|
||||
key={`gallery-${item.id}`}
|
||||
className="relative w-full aspect-[3/2] bg-gray-100 rounded overflow-hidden"
|
||||
>
|
||||
{item.image_url ? (
|
||||
<Image
|
||||
src={item.image_url}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="col-span-full text-center text-gray-400">
|
||||
Konten belum tersedia
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,42 @@ export default function HeaderAbout() {
|
|||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
{/* ===== Static Gallery Section ===== */}
|
||||
<div className="max-w-[1400px] mx-auto mt-32">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-3xl sm:text-4xl font-bold text-center mb-12"
|
||||
>
|
||||
Best Sales of The Month & SPV of The Month
|
||||
</motion.h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{["/Asset18.png", "/Asset19.png", "/Asset20.png", "/Asset21.png"].map(
|
||||
(src, index) => (
|
||||
<motion.div
|
||||
key={src}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative w-full overflow-hidden rounded-xl"
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={`static-gallery-${index}`}
|
||||
width={800}
|
||||
height={1000}
|
||||
className="w-full h-[auto] rounded-xl hover:scale-105 transition-transform duration-500"
|
||||
sizes="(max-width: 768px) 100vw, 400px"
|
||||
/>
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function HeaderPriceInformation() {
|
|||
},
|
||||
{
|
||||
title: "JAECOO J5 EV",
|
||||
image: "/j7-shs-nobg.png",
|
||||
image: "/price-j5.png",
|
||||
price: "Rp 299.900.000",
|
||||
oldPrice: "Rp 299.900.000",
|
||||
capacity: "60.9kWh",
|
||||
|
|
@ -45,7 +45,7 @@ export default function HeaderPriceInformation() {
|
|||
{
|
||||
title: "JAECOO J8 SHS-P ARDIS",
|
||||
image: "/j8-awd-nobg.png",
|
||||
price: "Rp 812.000.000",
|
||||
price: "Rp 828.000.000",
|
||||
oldPrice: "Rp 828.000.000",
|
||||
capacity: "34,46kWh",
|
||||
torque: "650Nm",
|
||||
|
|
|
|||
|
|
@ -14,28 +14,29 @@ import {
|
|||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HeaderProductJ7Awd() {
|
||||
export default function HeaderProductJ7Shs() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
|
||||
const [openBrosur, setOpenBrosur] = useState(false);
|
||||
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
// const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
// const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
// const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
// const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
|
||||
const images = [
|
||||
"/jj7-cars.png", // index 0
|
||||
"/green-j7-awd.png", // index 1
|
||||
"/black-j7-awd.png", // index 2
|
||||
"/white-j7-awd.png", // index 3
|
||||
"/jj7-blue.png",
|
||||
"/jj7-white.png",
|
||||
"/jj7-silver.png",
|
||||
"/jj7-black.png",
|
||||
];
|
||||
|
||||
const gradients = [
|
||||
"linear-gradient(to bottom, #B0B5C2, #B0B5C2)", // Hijau
|
||||
"linear-gradient(to bottom, #5D6B4F, #5D6B4F)", // Silver
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Putih
|
||||
"linear-gradient(to bottom, #527D97, #527D97)",
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)",
|
||||
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)",
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)",
|
||||
];
|
||||
return (
|
||||
<>
|
||||
|
|
@ -46,9 +47,9 @@ export default function HeaderProductJ7Awd() {
|
|||
transition={{ duration: 0.8 }}
|
||||
className="flex flex-col items-center gap-6"
|
||||
>
|
||||
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
|
||||
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[800px] overflow-hidden">
|
||||
<Image
|
||||
src="/product1.jpg"
|
||||
src="/shs-header.png"
|
||||
alt="about-header"
|
||||
fill
|
||||
className="object-cover"
|
||||
|
|
@ -56,88 +57,29 @@ export default function HeaderProductJ7Awd() {
|
|||
priority
|
||||
/>
|
||||
|
||||
{/* Tombol di dalam gambar, posisi bawah tengah */}
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
|
||||
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Link
|
||||
href="https://cms.jaecoo.id/uploads/Flyer_J7_SHS_6db27c3a25.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<DialogContent className=" w-full p-0 overflow-hidden">
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-end p-4 bg-white z-50">
|
||||
<a
|
||||
href={downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3"
|
||||
>
|
||||
<Download size={18} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Iframe Preview */}
|
||||
<iframe
|
||||
src={embedLink}
|
||||
className="w-full h-[70vh] border-t"
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Trigger untuk modal */}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1400px] h-[600px]">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
className=" object-fill"
|
||||
/>
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl text-center mb-4 font-bold">
|
||||
FORM TEST DRIVE
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
<Input placeholder="Mobile Number" />
|
||||
<Input placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-10">
|
||||
<Textarea placeholder="Full Message" rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-left ml-10">
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
className="bg-[#1F6779] text-white rounded-full"
|
||||
>
|
||||
SEND INQUIRY
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link
|
||||
href={`mailto:jaecookelapagading@gmail.com?subject=Test Drive J7 SHS-P &body=Halo Jaecoo,%0D%0A%0D%0ASaya tertarik untuk melakukan test drive kendaraan J7 SHS-P.%0D%0A%0D%0ANama:%0D%0ANomor HP:%0D%0ALokasi:%0D%0A%0D%0ATerima kasih.`}
|
||||
>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Section warna */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
|
|||
|
|
@ -14,28 +14,31 @@ import {
|
|||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HeaderProductJ7Shs() {
|
||||
export default function HeaderProductJ5Ev() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
|
||||
const [openBrosur, setOpenBrosur] = useState(false);
|
||||
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
// const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
// const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
// const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
// const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
|
||||
const images = [
|
||||
"/jj7-blue.png", // index 0
|
||||
"/jj7-white.png", // index 1
|
||||
"/jj7-silver.png", // index 2
|
||||
"/jj7-black.png", // index 3
|
||||
"/j5-putih.png",
|
||||
"/j5-hitam.png",
|
||||
"/j5-silver.png",
|
||||
"/j5-biru.png",
|
||||
"/j5-hijau.png",
|
||||
];
|
||||
|
||||
const gradients = [
|
||||
"linear-gradient(to bottom, #527D97, #527D97)", // Hijau
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Silver
|
||||
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)", // Putih
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)",
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)",
|
||||
"linear-gradient(to bottom, #B0B5C2, #B0B5C2)",
|
||||
"linear-gradient(to bottom, #233a77, #233a77)",
|
||||
"linear-gradient(to bottom, #5D6B4F, #5D6B4F)",
|
||||
];
|
||||
return (
|
||||
<>
|
||||
|
|
@ -46,9 +49,9 @@ export default function HeaderProductJ7Shs() {
|
|||
transition={{ duration: 0.8 }}
|
||||
className="flex flex-col items-center gap-6"
|
||||
>
|
||||
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[700px] overflow-hidden">
|
||||
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
|
||||
<Image
|
||||
src="/shs-header.png"
|
||||
src="/j5-new1.jpg"
|
||||
alt="about-header"
|
||||
fill
|
||||
className="object-cover"
|
||||
|
|
@ -56,88 +59,29 @@ export default function HeaderProductJ7Shs() {
|
|||
priority
|
||||
/>
|
||||
|
||||
{/* Tombol di dalam gambar, posisi bawah tengah */}
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
|
||||
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Link
|
||||
href="https://cms.jaecoo.id/uploads/Flyer_J5_EV_Ver3_smaller_file_size_d81b0f960c.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<DialogContent className=" w-full p-0 overflow-hidden">
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-end p-4 bg-white z-50">
|
||||
<a
|
||||
href={downloadLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3"
|
||||
>
|
||||
<Download size={18} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Iframe Preview */}
|
||||
<iframe
|
||||
src={embedLink}
|
||||
className="w-full h-[70vh] border-t"
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Trigger untuk modal */}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1400px] h-[600px]">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
className=" object-fill"
|
||||
/>
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl text-center mb-4 font-bold">
|
||||
FORM TEST DRIVE
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
<Input placeholder="Mobile Number" />
|
||||
<Input placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-10">
|
||||
<Textarea placeholder="Full Message" rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-left ml-10">
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
className="bg-[#1F6779] text-white rounded-full"
|
||||
>
|
||||
SEND INQUIRY
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link
|
||||
href={`mailto:jaecookelapagading@gmail.com?subject=Test Drive J5 EV &body=Halo Jaecoo,%0D%0A%0D%0ASaya tertarik untuk melakukan test drive kendaraan J5 EV.%0D%0A%0D%0ANama:%0D%0ANomor HP:%0D%0ALokasi:%0D%0A%0D%0ATerima kasih.`}
|
||||
>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Section warna */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
|
|||
|
|
@ -14,28 +14,29 @@ import {
|
|||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HeaderProductJ8Awd() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
|
||||
const [openBrosur, setOpenBrosur] = useState(false);
|
||||
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
// const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
|
||||
// const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
|
||||
// const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
|
||||
// const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
|
||||
|
||||
const images = [
|
||||
"/green.png", // index 0
|
||||
"/silver.png", // index 1
|
||||
"/white.png", // index 3
|
||||
"/black.png", // index 2
|
||||
"/j8-green-ardis.png",
|
||||
"/j8-silver-ardis.png",
|
||||
"/j8-white-ardis.png",
|
||||
"/j8-ardis-black.png",
|
||||
];
|
||||
|
||||
const gradients = [
|
||||
"linear-gradient(to bottom, #527D97, #1F6779)", // Hijau
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Silver
|
||||
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)", // Putih
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
|
||||
"linear-gradient(to bottom, #527D97, #1F6779)",
|
||||
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)",
|
||||
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)",
|
||||
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)",
|
||||
];
|
||||
return (
|
||||
<>
|
||||
|
|
@ -56,9 +57,8 @@ export default function HeaderProductJ8Awd() {
|
|||
priority
|
||||
/>
|
||||
|
||||
{/* Tombol di dalam gambar, posisi bawah tengah */}
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
|
||||
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
|
||||
{/* <Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
|
|
@ -66,7 +66,6 @@ export default function HeaderProductJ8Awd() {
|
|||
</DialogTrigger>
|
||||
|
||||
<DialogContent className=" w-full p-0 overflow-hidden">
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-end p-4 bg-white z-50">
|
||||
<a
|
||||
href={downloadLink}
|
||||
|
|
@ -78,66 +77,34 @@ export default function HeaderProductJ8Awd() {
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{/* Iframe Preview */}
|
||||
<iframe
|
||||
src={embedLink}
|
||||
className="w-full h-[70vh] border-t"
|
||||
allow="autoplay"
|
||||
></iframe>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Trigger untuk modal */}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1400px] h-[600px]">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
className=" object-fill"
|
||||
/>
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl text-center mb-4 font-bold">
|
||||
FORM TEST DRIVE
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
<Input placeholder="Mobile Number" />
|
||||
<Input placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-10">
|
||||
<Textarea placeholder="Full Message" rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-left ml-10">
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
className="bg-[#1F6779] text-white rounded-full"
|
||||
>
|
||||
SEND INQUIRY
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog> */}
|
||||
<Link
|
||||
href="https://cms.jaecoo.id/uploads/J8_SHS_ARDIS_Flyer_cbf280ea77.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
|
||||
BROSUR
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={`mailto:jaecookelapagading@gmail.com?subject=Test Drive J8 SHS-P ARDIS &body=Halo Jaecoo,%0D%0A%0D%0ASaya tertarik untuk melakukan test drive kendaraan J8 SHS-P ARDIS.%0D%0A%0D%0ANama:%0D%0ANomor HP:%0D%0ALokasi:%0D%0A%0D%0ATerima kasih.`}
|
||||
>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* Section warna */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Carousel,
|
||||
|
|
@ -19,131 +20,85 @@ import {
|
|||
} from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Autoplay from "embla-carousel-autoplay"; // ✅ Import plugin autoplay
|
||||
import { useRef } from "react";
|
||||
import { getBannerData } from "@/service/banner";
|
||||
|
||||
const heroImages = ["/Hero.png", "/hero-bdg2.png", "/hero-bdg3.png"];
|
||||
const heroImages = [
|
||||
"/Hero.png",
|
||||
// "/Carousell-01.png",
|
||||
"/Carousell-02.png",
|
||||
"/Carousell-03.png",
|
||||
"/Carousell-04.png",
|
||||
"/Carousell-05.png",
|
||||
];
|
||||
type Banner = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status_id: number;
|
||||
position: string;
|
||||
thumbnail_url: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export default function Header() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [banners, setBanners] = useState<Banner[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBanners = async () => {
|
||||
try {
|
||||
const req = {
|
||||
limit: "10",
|
||||
page: 1,
|
||||
search: "",
|
||||
};
|
||||
|
||||
const res = await getBannerData(req);
|
||||
|
||||
const bannerData: Banner[] = res?.data?.data || [];
|
||||
|
||||
const activeBanners = bannerData
|
||||
.filter((banner) => banner.status_id === 2) // ✅ approved only
|
||||
.sort((a, b) => {
|
||||
const posA = Number(a.position);
|
||||
const posB = Number(b.position);
|
||||
|
||||
if (posA !== posB) return posA - posB;
|
||||
|
||||
return (
|
||||
new Date(b.created_at).getTime() -
|
||||
new Date(a.created_at).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
setBanners(activeBanners);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch banners:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBanners();
|
||||
}, []);
|
||||
|
||||
// ✅ Gunakan useRef untuk plugin autoplay
|
||||
const plugin = useRef(Autoplay({ delay: 4000, stopOnInteraction: false }));
|
||||
|
||||
return (
|
||||
<section className="relative w-full overflow-hidden bg-white">
|
||||
<Carousel
|
||||
className="w-full relative"
|
||||
plugins={[plugin.current]} // ✅ Tambahkan plugin di sini
|
||||
>
|
||||
<section className="relative w-full overflow-hidden bg-white mt-5">
|
||||
<Carousel className="w-full relative" plugins={[plugin.current]}>
|
||||
<CarouselContent>
|
||||
{heroImages.map((img, index) => (
|
||||
<CarouselItem key={index}>
|
||||
<div className="relative w-full h-[400px] sm:h-[500px] md:h-[810px]">
|
||||
{banners.map((banner, index) => (
|
||||
<CarouselItem key={banner.id}>
|
||||
<div className="relative w-full aspect-[16/9]">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`JAECOO Image ${index + 1}`}
|
||||
width={1400}
|
||||
height={810}
|
||||
className="object-cover w-full h-full"
|
||||
src={banner.thumbnail_url}
|
||||
alt={banner.title}
|
||||
fill
|
||||
priority={index === 0}
|
||||
className="object-contain"
|
||||
/>
|
||||
|
||||
{index === 0 && (
|
||||
<div className="absolute inset-0 flex flex-col justify-center items-start px-4 sm:px-8 md:px-28 z-10">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="text-2xl sm:text-3xl md:text-5xl font-bold text-black mb-4"
|
||||
>
|
||||
JAECOO J7 SHS-P
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.9,
|
||||
ease: "easeOut",
|
||||
delay: 0.2,
|
||||
}}
|
||||
className="text-sm sm:text-base md:text-lg text-black mb-6"
|
||||
>
|
||||
DELICATE OFF-ROAD SUV
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, ease: "easeOut", delay: 0.4 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-center gap-4"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: "easeOut",
|
||||
delay: 0.4,
|
||||
}}
|
||||
>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1400px] h-[600px]">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
className=" object-fill"
|
||||
/>
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-center mb-4">
|
||||
FORM TEST DRIVE
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
<Input placeholder="Mobile Number" />
|
||||
<Input placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-10">
|
||||
<Textarea placeholder="Full Message" rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-left ml-10">
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
className="bg-[#1F6779] text-white rounded-full"
|
||||
>
|
||||
SEND INQUIRY
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link href={"/product"}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="rounded-full border-black text-black px-6 py-2 hover:cursor-pointer hover:bg-amber-50"
|
||||
>
|
||||
EXPLORE
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import { useInView } from "react-intersection-observer";
|
|||
|
||||
const featuresInt = [
|
||||
{
|
||||
title: "14.8 Screen with APPLE Carplay & Android Auto",
|
||||
description:
|
||||
"Stay connected and informed with a 14.8 display offering clear visuals and advanced functionality for a seamless driving experience.",
|
||||
image: "/in-shs2.png",
|
||||
title: "13.2 Screen with APPLE Carplay & Android Auto",
|
||||
description: "13.2 Full HD Center Display",
|
||||
image: "/headunit.png",
|
||||
},
|
||||
{
|
||||
title: "Horizontal Side by Side Cup Holder",
|
||||
|
|
@ -24,9 +23,8 @@ const featuresInt = [
|
|||
},
|
||||
{
|
||||
title: "Wireless Charging",
|
||||
description:
|
||||
"Stay powered up on the go with Wireless Charging, ensuring your devices are always ready when you are.",
|
||||
image: "/in-shs5.png",
|
||||
description: "Fast Wireless Charging 50W",
|
||||
image: "/charging.png",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -78,7 +76,7 @@ export default function InteriorShs() {
|
|||
transition={{ duration: 0.7 }}
|
||||
>
|
||||
<Image
|
||||
src="/in-shs.png"
|
||||
src="/inter.png"
|
||||
alt="Interior Hero"
|
||||
fill
|
||||
className="object-cover"
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ const items = [
|
|||
image: "/new-car2.png",
|
||||
title: "JAECOO J7 SHS-P",
|
||||
description: "DELICATE OFF-ROAD SUV",
|
||||
link: "/product/j7-awd",
|
||||
link: "/product/j7-shs-p",
|
||||
},
|
||||
{
|
||||
image: "/new-car1.png",
|
||||
image: "/j5-ev-new.png",
|
||||
title: "JAECOO J5 EV",
|
||||
description: "SUPER HYBRID SYSTEM = SUPER HEV + EV",
|
||||
link: "/product/j7-shs",
|
||||
link: "/product/j5-ev",
|
||||
},
|
||||
{
|
||||
image: "/new-car3.png",
|
||||
|
|
@ -87,7 +87,7 @@ export default function Items() {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.25 + 0.6, duration: 0.6 }}
|
||||
>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{/* <Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
|
|
@ -109,7 +109,7 @@ export default function Items() {
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
|
|
@ -130,7 +130,15 @@ export default function Items() {
|
|||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog> */}
|
||||
<Link
|
||||
href={`mailto:jaecookelapagading@gmail.com?subject=Test Drive ${item.title}&body=Halo Jaecoo,%0D%0A%0D%0ASaya tertarik untuk melakukan test drive kendaraan ${item.title}.%0D%0A%0D%0ANama:%0D%0ANomor HP:%0D%0ALokasi:%0D%0A%0D%0ATerima kasih.`}
|
||||
>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={item?.link}>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default function Navbar() {
|
|||
<div className="flex items-center gap-4">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
src="/jaecoonew.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
|
|
@ -195,50 +195,13 @@ export default function Navbar() {
|
|||
/>
|
||||
<p className="font-bold mt-4 text-center">{car.name}</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2 mt-2 items-center">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1400px] h-[600px]">
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
src="/masjaecoonav.png"
|
||||
alt="MAS JAECOO Logo"
|
||||
width={300}
|
||||
height={30}
|
||||
className=" object-fill"
|
||||
/>
|
||||
</div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl text-center mb-4 font-bold">
|
||||
FORM TEST DRIVE
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
|
||||
<Input placeholder="Nama" />
|
||||
<Input placeholder="Email" />
|
||||
<Input placeholder="Mobile Number" />
|
||||
<Input placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-10">
|
||||
<Textarea placeholder="Full Message" rows={4} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-left ml-10">
|
||||
<Button
|
||||
onClick={() => setOpen(false)}
|
||||
className="bg-[#1F6779] text-white rounded-full"
|
||||
>
|
||||
SEND INQUIRY
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link
|
||||
href={`mailto:jaecookelapagading@gmail.com?subject=Test Drive ${car?.name} &body=Halo Jaecoo,%0D%0A%0D%0ASaya tertarik untuk melakukan test drive kendaraan ${car?.name}.%0D%0A%0D%0ANama:%0D%0ANomor HP:%0D%0ALokasi:%0D%0A%0D%0ATerima kasih.`}
|
||||
>
|
||||
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
|
||||
TEST DRIVE
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={car.link} className="w-[200px]">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -2,149 +2,128 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
const tabs = ["INSTAGRAM", "TIKTOK", "FACEBOOK", "YOUTUBE"];
|
||||
const tabs = ["INSTAGRAM", "TIKTOK"];
|
||||
|
||||
const instagramPosts = ["/ig1-new.png", "/ig2-new.png", "/ig3-new.png"];
|
||||
const tiktokPosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
|
||||
const youtubePosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
|
||||
const facebookPosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
|
||||
const instagramPosts = ["/ig1-new1.png", "/ig2-new2.png", "/ig3-new3.png"];
|
||||
|
||||
const tiktokPosts = [
|
||||
"/tiktokchm1.jpeg",
|
||||
"/tiktokchm2.jpeg",
|
||||
"/tiktokchm3.jpeg",
|
||||
];
|
||||
|
||||
export default function SosmedSection() {
|
||||
const [activeTab, setActiveTab] = useState("INSTAGRAM");
|
||||
|
||||
return (
|
||||
<section className="px-4 py-16 max-w-[1400px] mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-6 text-center">Sosial Media Kami</h2>
|
||||
{/* Title */}
|
||||
<h2 className="text-3xl font-bold mb-2 ml-4">Sosial Media Kami</h2>
|
||||
<p className="text-gray-500 text-sm mb-8 ml-4">
|
||||
Preview konten dari media sosial resmi kami
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center justify-center mb-8">
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-4 justify-center mb-10">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`text-sm font-medium px-4 py-2 rounded-full ${
|
||||
activeTab === tab
|
||||
? "bg-[#BCD4DF] text-sky-700"
|
||||
: "text-[gray-700] hover:bg-gray-100"
|
||||
}`}
|
||||
className={`px-5 py-2 rounded-full text-sm font-medium transition
|
||||
${
|
||||
activeTab === tab
|
||||
? "bg-[#BCD4DF] text-sky-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* INSTAGRAM */}
|
||||
{activeTab === "INSTAGRAM" && (
|
||||
<>
|
||||
<div className="flex flex-wrap justify-center items-center gap-4">
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{instagramPosts.map((img, i) => (
|
||||
<div
|
||||
<Link
|
||||
key={i}
|
||||
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
|
||||
href="https://www.instagram.com/jaecoocihampelasbdg"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Instagram post ${i + 1}`}
|
||||
fill
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full sm:w-[300px] md:w-[340px] aspect-[4/5] rounded-xl overflow-hidden bg-gray-100 group cursor-pointer shadow-sm">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Instagram post ${i + 1}`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
|
||||
<span className="text-white text-sm font-semibold">
|
||||
View on Instagram
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-10">
|
||||
<Link href={"https://www.instagram.com/jaecoo_cihampelasbdg"}>
|
||||
<div className="flex justify-center mt-12">
|
||||
<Link
|
||||
href="https://www.instagram.com/jaecoocihampelasbdg"
|
||||
target="_blank"
|
||||
>
|
||||
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
|
||||
Lihat Selengkapnya
|
||||
<ArrowRight size={35} />
|
||||
<ArrowRight size={28} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TIKTOK */}
|
||||
{activeTab === "TIKTOK" && (
|
||||
<>
|
||||
<div className="flex flex-wrap justify-center items-center gap-4">
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{tiktokPosts.map((img, i) => (
|
||||
<div
|
||||
<Link
|
||||
key={i}
|
||||
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
|
||||
href="https://www.tiktok.com/@jaecoo.cihampelasbdg"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Tiktok post ${i + 1}`}
|
||||
fill
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full sm:w-[260px] md:w-[300px] aspect-[9/16] rounded-xl overflow-hidden bg-gray-100 group cursor-pointer shadow-sm">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`TikTok post ${i + 1}`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
|
||||
<span className="text-white text-sm font-semibold">
|
||||
View on TikTok
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-10">
|
||||
<Link href={"https://www.tiktok.com/@jaecoo.cihampelasbdg"}>
|
||||
<div className="flex justify-center mt-12">
|
||||
<Link
|
||||
href="https://www.tiktok.com/@jaecoo.cihampelasbdg"
|
||||
target="_blank"
|
||||
>
|
||||
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
|
||||
Lihat Selengkapnya
|
||||
<ArrowRight size={35} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "FACEBOOK" && (
|
||||
<>
|
||||
<div className="flex flex-wrap justify-center items-center gap-4">
|
||||
{facebookPosts.map((img, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Facebook post ${i + 1}`}
|
||||
fill
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-10">
|
||||
<Link href={"#"}>
|
||||
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
|
||||
Lihat Selengkapnya
|
||||
<ArrowRight size={35} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "YOUTUBE" && (
|
||||
<>
|
||||
<div className="flex flex-wrap justify-center items-center gap-4">
|
||||
{youtubePosts.map((img, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`YouTube post ${i + 1}`}
|
||||
fill
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-10">
|
||||
<Link href={"#"}>
|
||||
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
|
||||
Lihat Selengkapnya
|
||||
<ArrowRight size={35} />
|
||||
<ArrowRight size={28} />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import { getBannerData } from "@/service/banner";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Banner = {
|
||||
id: number;
|
||||
title: string;
|
||||
status_id: number;
|
||||
thumbnail_url: string;
|
||||
};
|
||||
|
||||
export default function Video() {
|
||||
const [banner, setBanner] = useState<Banner | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRandomBanner = async () => {
|
||||
try {
|
||||
const req = {
|
||||
limit: "20", // ambil agak banyak biar random lebih terasa
|
||||
page: 1,
|
||||
search: "",
|
||||
};
|
||||
|
||||
const res = await getBannerData(req);
|
||||
|
||||
const banners: Banner[] = res?.data?.data || [];
|
||||
|
||||
const approvedBanners = banners.filter((item) => item.status_id === 2);
|
||||
|
||||
if (approvedBanners.length === 0) return;
|
||||
|
||||
const randomBanner =
|
||||
approvedBanners[Math.floor(Math.random() * approvedBanners.length)];
|
||||
|
||||
setBanner(randomBanner);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch random banner:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRandomBanner();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="pt-10 bg-white">
|
||||
<div className="relative mb-10 w-full h-[250px] sm:h-[400px] md:h-[600px]">
|
||||
<div className="relative mb-10 w-full aspect-[16/9]">
|
||||
<Image
|
||||
src={"/maintenance.png"}
|
||||
alt="maintenance"
|
||||
src={banner?.thumbnail_url || "/maintenance.png"}
|
||||
alt={banner?.title || "Banner"}
|
||||
fill
|
||||
className="object-cover"
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -512,7 +512,7 @@ export default function ProductTable() {
|
|||
</Button>
|
||||
</Link>
|
||||
{userRoleId !== "1" && (
|
||||
<Link href={"/admin/product/update"}>
|
||||
<Link href={`/admin/product/update/${item.id}`}>
|
||||
<Button
|
||||
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import {
|
|||
approvePromotion,
|
||||
rejectPromotion,
|
||||
} from "@/service/promotion";
|
||||
import PromoEditDialog from "../dialog/promo-edit-dialog";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
|
|
@ -95,6 +96,11 @@ export default function PromotionTable() {
|
|||
const [categories, setCategories] = useState<any>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||
const [openDetail, setOpenDetail] = useState(false);
|
||||
const [openEditPromo, setOpenEditPromo] = useState(false);
|
||||
const [selectedEditPromoId, setSelectedEditPromoId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [selectedPromo, setPromoDetail] = useState<any>(null);
|
||||
const [selectedPromoId, setSelectedPromoId] = useState<number | null>(null);
|
||||
const [startDateValue, setStartDateValue] = useState({
|
||||
|
|
@ -474,7 +480,19 @@ export default function PromotionTable() {
|
|||
<Eye className="w-4 h-4 mr-1" /> Lihat
|
||||
</Button>
|
||||
{/* Tombol Edit */}
|
||||
|
||||
{userRoleId !== "1" && (
|
||||
<Button
|
||||
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedEditPromoId(item.id);
|
||||
setOpenEditPromo(true);
|
||||
}}
|
||||
>
|
||||
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
|
||||
</Button>
|
||||
)}
|
||||
{/* Tombol Approve & Reject - hanya untuk admin dan status pending */}
|
||||
{/* {userRoleId === "1" && item.status_id === 1 && (
|
||||
<>
|
||||
|
|
@ -529,6 +547,15 @@ export default function PromotionTable() {
|
|||
open={openDetail}
|
||||
onOpenChange={setOpenDetail}
|
||||
/>
|
||||
<PromoEditDialog
|
||||
promoId={selectedEditPromoId}
|
||||
open={openEditPromo}
|
||||
onOpenChange={setOpenEditPromo}
|
||||
onSuccess={() => {
|
||||
setOpenEditPromo(false);
|
||||
initState(); // refresh table setelah edit
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* FOOTER PAGINATION */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: [
|
||||
"mikulnews.com",
|
||||
"dev.mikulnews.com",
|
||||
"jaecoocihampelasbdg.com",
|
||||
"jaecookelapagading.com",
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "jaecoocihampelasbdg.com",
|
||||
pathname: "/api/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "jaecookelapagading.com",
|
||||
pathname: "/api/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
|
||||
experimental: {
|
||||
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2834,6 +2834,7 @@
|
|||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
|
|
@ -2841,7 +2842,7 @@
|
|||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.6",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
|
@ -0,0 +1 @@
|
|||
google-site-verification: google162462b69256f396.html
|
||||
|
|
@ -1 +0,0 @@
|
|||
google-site-verification: googlee3f6c9cb1d4e657b.html
|
||||
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 8.0 MiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 520 KiB |
|
After Width: | Height: | Size: 9.6 MiB |
|
After Width: | Height: | Size: 519 KiB |
|
After Width: | Height: | Size: 525 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
|
@ -17,6 +17,11 @@ export async function getGaleryFileData(props: PaginationRequest) {
|
|||
return await httpGetInterceptor(`/gallery-files`);
|
||||
}
|
||||
|
||||
// untuk list files
|
||||
export async function getAllGaleryFiles() {
|
||||
return await httpGetInterceptor(`/gallery-files`);
|
||||
}
|
||||
|
||||
export async function getGaleryById(id: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import axios from "axios";
|
|||
const baseURL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecoocihampelasbdg.com/api";
|
||||
const clientKey =
|
||||
process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
|
||||
process.env.NEXT_PUBLIC_CLIENT_KEY || "92387f2e-d45f-49e0-b663-66e5d2878d00";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Cookies from "js-cookie";
|
|||
const baseURL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecoocihampelasbdg.com/api";
|
||||
const clientKey =
|
||||
process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
|
||||
process.env.NEXT_PUBLIC_CLIENT_KEY || "92387f2e-d45f-49e0-b663-66e5d2878d00";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import axiosBaseInstance from "./axios-base-instance";
|
|||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
|
||||
"X-Client-Key": "92387f2e-d45f-49e0-b663-66e5d2878d00",
|
||||
};
|
||||
|
||||
export async function httpGet(pathUrl: any, headers?: any) {
|
||||
console.log("X-HEADERS : ", defaultHeaders)
|
||||
console.log("X-HEADERS : ", defaultHeaders);
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { getCsrfToken } from "../master-user";
|
|||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
|
||||
"X-Client-Key": "92387f2e-d45f-49e0-b663-66e5d2878d00",
|
||||
};
|
||||
|
||||
export async function httpGetInterceptor(pathUrl: any) {
|
||||
console.log("X-HEADERS : ", defaultHeaders)
|
||||
console.log("X-HEADERS : ", defaultHeaders);
|
||||
const response = await axiosInterceptorInstance
|
||||
.get(pathUrl, { headers: defaultHeaders })
|
||||
.catch((error) => error.response);
|
||||
|
|
@ -35,7 +35,11 @@ export async function httpGetInterceptor(pathUrl: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function httpPostInterceptor(pathUrl: any, data: any, headers?: any) {
|
||||
export async function httpPostInterceptor(
|
||||
pathUrl: any,
|
||||
data: any,
|
||||
headers?: any,
|
||||
) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
|
|
@ -67,7 +71,11 @@ export async function httpPostInterceptor(pathUrl: any, data: any, headers?: any
|
|||
}
|
||||
}
|
||||
|
||||
export async function httpPutInterceptor(pathUrl: any, data: any, headers?: any) {
|
||||
export async function httpPutInterceptor(
|
||||
pathUrl: any,
|
||||
data: any,
|
||||
headers?: any,
|
||||
) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
|
|
@ -99,7 +107,7 @@ export async function httpPutInterceptor(pathUrl: any, data: any, headers?: any)
|
|||
}
|
||||
|
||||
export async function httpDeleteInterceptor(pathUrl: any, headers?: any) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
|
|
|
|||