Compare commits

...

10 Commits

Author SHA1 Message Date
Anang Yusman a94af4cce6 update price 2026-02-09 10:43:37 +08:00
Anang Yusman 406c166525 update agent 2026-02-05 21:55:37 +08:00
Anang Yusman daf0c5d20c update agent 2026-02-05 19:14:16 +08:00
Anang Yusman 1614997b52 add filter category 2026-02-03 19:30:03 +08:00
Anang Yusman e2accd9599 update landing 2026-02-03 14:28:00 +08:00
Anang Yusman c5230fdc16 fixing 2026-02-03 01:44:13 +08:00
Anang Yusman 3cff827c58 update api landingpage 2026-02-02 21:43:48 +08:00
Anang Yusman f8abbeace1 update banner,agent landingpage 2026-02-02 16:08:56 +08:00
Anang Yusman 407020ecb3 feat:update select color, and gsc 2026-01-29 16:37:51 +08:00
Anang Yusman be38482c88 fix:product,promo form 2026-01-29 02:19:14 +08:00
58 changed files with 1216 additions and 883 deletions

View File

@ -8,7 +8,6 @@ export default function AboutPage() {
<div className="relative z-10 bg-white w-full mx-auto"> <div className="relative z-10 bg-white w-full mx-auto">
<Navbar /> <Navbar />
<GallerySection /> <GallerySection />
<Footer /> <Footer />
</div> </div>
</div> </div>

View File

@ -16,7 +16,6 @@ export default function Home() {
<Items /> <Items />
<Video /> <Video />
<Agent /> <Agent />
{/* <Location /> */}
<Footer /> <Footer />
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import ExteriorShs from "@/components/landing-page/exterior-shs"; import ExteriorShs from "@/components/landing-page/exterior-shs";
import FeaturesAndSpecificationsShs from "@/components/landing-page/features-and-specifications-shs"; import FeaturesAndSpecificationsShs from "@/components/landing-page/features-and-specifications-shs";
import Footer from "@/components/landing-page/footer"; 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 HeaderProductJ7Shs from "@/components/landing-page/header-product-j7-shs";
import InteriorShs from "@/components/landing-page/interior-shs"; import InteriorShs from "@/components/landing-page/interior-shs";
import Navbar from "@/components/landing-page/navbar"; 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 min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto"> <div className="relative z-10 bg-white w-full mx-auto">
<Navbar /> <Navbar />
<HeaderProductJ7Shs /> <HeaderProductJ5Ev />
<ExteriorShs /> <ExteriorShs />
<InteriorShs /> <InteriorShs />
<FeaturesAndSpecificationsShs /> <FeaturesAndSpecificationsShs />

View File

@ -13,12 +13,22 @@ import { Upload, X } from "lucide-react";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { createGalery, uploadGaleryFile } from "@/service/galery"; import { createGalery, uploadGaleryFile } from "@/service/galery";
const CATEGORY_OPTIONS = [
"Grand Opening",
"IIMS",
"GIIAS",
"GJAW",
"Exhibitions",
"Test Drive",
];
export function GaleriDialog({ open, onClose, onSubmit }: any) { export function GaleriDialog({ open, onClose, onSubmit }: any) {
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [previews, setPreviews] = useState<string[]>([]); const [previews, setPreviews] = useState<string[]>([]);
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
const [category, setCategory] = useState("");
useEffect(() => { useEffect(() => {
if (!files || files.length === 0) { if (!files || files.length === 0) {
@ -47,10 +57,11 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!title) return alert("Judul wajib diisi!"); if (!title) return alert("Judul wajib diisi!");
if (!category) return alert("Category wajib diisi!");
const formData = new FormData(); const formData = new FormData();
formData.append("title", title); formData.append("title", title);
formData.append("description", description); formData.append("description", description);
formData.append("category", category);
const res = await createGalery(formData); const res = await createGalery(formData);
@ -73,7 +84,7 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
} }
onSubmit(); onSubmit();
setCategory("");
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setFiles([]); setFiles([]);
@ -106,6 +117,26 @@ export function GaleriDialog({ open, onClose, onSubmit }: any) {
/> />
</div> </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 */} {/* Deskripsi */}
<div> <div>
<label className="font-medium text-sm"> <label className="font-medium text-sm">

View File

@ -289,7 +289,7 @@ export default function PromoDetailDialog({
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
<div> <div>
<div className="w-24 h-24 rounded-lg overflow-hidden border"> <div className="w-24 h-24 rounded-lg overflow-hidden border">
<Image <img
src={promo.thumbnail_url} src={promo.thumbnail_url}
alt="Profile" alt="Profile"
width={96} width={96}

View File

@ -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>
);
}

View File

@ -278,7 +278,7 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
<div> <div>
<Label className="mb-2 block">Foto Profile</Label> <Label className="mb-2 block">Foto Profile</Label>
<div className="w-24 h-24 rounded-lg overflow-hidden border"> <div className="w-24 h-24 rounded-lg overflow-hidden border">
<Image <img
src={data?.profile_picture_url} src={data?.profile_picture_url}
alt="Profile" alt="Profile"
width={96} width={96}

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
@ -23,6 +23,7 @@ import { createProduct } from "@/service/product";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { loading } from "@/config/swal";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1, "Nama produk wajib diisi"), name: z.string().min(1, "Nama produk wajib diisi"),
@ -36,18 +37,20 @@ export default function AddProductForm() {
{ id: number; name: string; file: File | null }[] { id: number; name: string; file: File | null }[]
>([{ id: 1, name: "", file: null }]); >([{ id: 1, name: "", file: null }]);
const [selectedColor, setSelectedColor] = useState<string | null>(null); // const [selectedColor, setSelectedColor] = useState<string | null>(null);
const [specs, setSpecs] = useState< const [specs, setSpecs] = useState<
{ id: number; title: string; images: string[]; files: File[] }[] { id: number; title: string; images: string[]; files: File[] }[]
>([{ id: 1, title: "", images: [], files: [] }]); >([{ id: 1, title: "", images: [], files: [] }]);
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [uploadTarget, setUploadTarget] = useState<{ const [uploadTarget, setUploadTarget] = useState<{
type: "spec"; type: "spec";
index: number; index: number;
} | null>(null); } | null>(null);
const fileInputId = "spec-upload-input"; const fileInputId = "spec-upload-input";
const fileInputRef = useRef<HTMLInputElement | null>(null);
const isUploadingRef = useRef(false);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
@ -80,30 +83,57 @@ export default function AddProductForm() {
setSpecs(updated); setSpecs(updated);
}; };
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const input = e.target;
if (!file || !uploadTarget) return; const selectedFile = input.files?.[0];
const reader = new FileReader(); if (!selectedFile || !uploadTarget) return;
reader.onload = () => {
const fileUrl = reader.result as string;
if (uploadTarget.type === "spec") { // 🔒 CEGAH DOUBLE EVENT
setSpecs((prev) => { if (isUploadingRef.current) return;
const updated = [...prev]; isUploadingRef.current = true;
updated[uploadTarget.index].images.push(fileUrl);
updated[uploadTarget.index].files.push(file); setSpecs((prev) => {
return updated; const updated = [...prev];
}); const spec = updated[uploadTarget.index];
// max 5 gambar
if (spec.files.length >= 5) {
isUploadingRef.current = false;
return prev;
} }
};
reader.readAsDataURL(file); // 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); setIsUploadDialogOpen(false);
// release lock
setTimeout(() => {
isUploadingRef.current = false;
}, 0);
}; };
const onSubmit = async (data: z.infer<typeof formSchema>) => { const onSubmit = async (data: z.infer<typeof formSchema>) => {
try { try {
loading();
const formData = new FormData(); const formData = new FormData();
formData.append("title", data.name); formData.append("title", data.name);
@ -204,7 +234,9 @@ export default function AddProductForm() {
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div> <div>
<Label>Nama Produk *</Label> <Label>
Nama Produk <span className="text-red-500">*</span>
</Label>
<Input placeholder="Masukkan Nama Produk" {...register("name")} /> <Input placeholder="Masukkan Nama Produk" {...register("name")} />
{errors.name && ( {errors.name && (
<p className="text-sm text-red-500 mt-1"> <p className="text-sm text-red-500 mt-1">
@ -214,7 +246,9 @@ export default function AddProductForm() {
</div> </div>
<div> <div>
<Label>Tipe Varian *</Label> <Label>
Tipe Varian <span className="text-red-500">*</span>
</Label>
<Input <Input
placeholder="Contoh: AWD, SHS, EV" placeholder="Contoh: AWD, SHS, EV"
{...register("variant")} {...register("variant")}
@ -227,7 +261,9 @@ export default function AddProductForm() {
</div> </div>
<div> <div>
<Label>Harga Produk *</Label> <Label>
Harga Produk <span className="text-red-500">*</span>
</Label>
<Input <Input
placeholder="Masukkan Harga Produk" placeholder="Masukkan Harga Produk"
value={priceDisplay} value={priceDisplay}
@ -272,7 +308,9 @@ export default function AddProductForm() {
{/* Upload Produk */} {/* Upload Produk */}
<div> <div>
<Label>Upload Produk *</Label> <Label>
Upload Produk <span className="text-red-500">*</span>
</Label>
{colors.map((color, index) => ( {colors.map((color, index) => (
<div <div
key={color.id} key={color.id}
@ -312,15 +350,16 @@ export default function AddProductForm() {
type="button" type="button"
onClick={() => { onClick={() => {
const updated = [...colors]; const updated = [...colors];
updated[index].name = colorCode; // 🔥 INI KUNCINYA updated[index].name = colorCode;
setColors(updated); setColors(updated);
setSelectedColor(colorCode);
}} }}
className={`w-8 h-8 rounded-full border-2 transition ${ className={`w-8 h-8 rounded-full border-2 transition
selectedColor === colorCode ${
? "border-teal-700 scale-110" colors[index].name === colorCode
: "border-gray-200" ? "border-teal-700 scale-110"
}`} : "border-gray-200"
}
`}
style={{ backgroundColor: colorCode }} style={{ backgroundColor: colorCode }}
/> />
))} ))}
@ -370,7 +409,10 @@ export default function AddProductForm() {
</Label> </Label>
{specs.map((spec, index) => ( {specs.map((spec, index) => (
<div key={spec.id} className="mt-6 border-2 rounded-lg border-black p-3"> <div
key={spec.id}
className="mt-6 border-2 rounded-lg border-black p-3"
>
<Label className="text-sm font-semibold"> <Label className="text-sm font-semibold">
Judul Spesifikasi {index + 1} Judul Spesifikasi {index + 1}
</Label> </Label>
@ -456,36 +498,32 @@ export default function AddProductForm() {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div <div className="border-2 border-dashed rounded-xl p-8 text-center">
className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer"
onClick={() => document.getElementById(fileInputId)?.click()}
>
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" /> <Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Klik untuk upload atau drag & drop Klik tombol di bawah untuk memilih file
</p> </p>
<p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p> <p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p>
<input
id={fileInputId}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelected}
/>
</div> </div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelected}
/>
<DialogFooter className="flex gap-3 pt-4"> <DialogFooter className="flex gap-3 pt-4">
<Button <Button
variant="secondary" variant="secondary"
className="bg-slate-200"
onClick={() => setIsUploadDialogOpen(false)} onClick={() => setIsUploadDialogOpen(false)}
> >
Batal Batal
</Button> </Button>
<Button <Button
onClick={() => document.getElementById(fileInputId)?.click()} onClick={() => fileInputRef.current?.click()}
className="bg-teal-800 hover:bg-teal-900 text-white" className="bg-teal-800 hover:bg-teal-900 text-white"
> >
Pilih File Pilih File

View File

@ -159,13 +159,13 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
setError, setError,
clearErrors, clearErrors,
} = useForm<UserSettingSchema>(formOptions); } = useForm<UserSettingSchema>(formOptions);
const [specs, setSpecs] = useState([ const [specs, setSpecs] = useState<
{ {
id: 1, id: number;
title: "Jaecoo 7 SHS Teknologi dan Exterior", title: string;
images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"], images: string[];
}, }[]
]); >([]);
type ColorType = { type ColorType = {
id: number; id: number;
@ -174,20 +174,7 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
colorSelected: string | null; colorSelected: string | null;
}; };
const [colors, setColors] = useState<ColorType[]>([ const [colors, setColors] = useState<ColorType[]>([]);
{
id: 1,
name: "",
preview: "/car-1.png",
colorSelected: null,
},
{
id: 2,
name: "",
preview: "/car-2.png",
colorSelected: null,
},
]);
const palette = [ const palette = [
"#1E4E52", "#1E4E52",
@ -297,24 +284,31 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
setThumbnail(data.thumbnail_url); setThumbnail(data.thumbnail_url);
// colors // colors
if (data.colors?.length) { if (Array.isArray(data.colors)) {
setColors( setColors(
data.colors.map((color: any, index: number) => ({ data.colors.map((color: any, index: number) => ({
id: index + 1, id: color.id ?? index + 1,
name: color.name || "", name: color.name ?? "",
preview: color.image_url || data.thumbnail_url || "", preview:
colorSelected: color.name || null, typeof color.image_url === "string" && color.image_url.length > 0
? color.image_url
: "/car-default.png", // fallback aman
colorSelected: color.name ?? null,
})), })),
); );
} }
// specifications // specifications
if (data.specifications?.length) { if (Array.isArray(data.specifications)) {
setSpecs( setSpecs(
data.specifications.map((spec: any, index: number) => ({ data.specifications.map((spec: any, index: number) => ({
id: index + 1, id: index + 1,
title: spec.title || "", title: spec.title ?? "",
images: spec.image_urls || [], images: Array.isArray(spec.image_urls)
? spec.image_urls.filter(
(url: any) => typeof url === "string" && url.length > 0,
)
: [],
})), })),
); );
} }
@ -520,36 +514,44 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
<div> <div>
<Label className="font-semibold text-gray-700">Warna Produk</Label> <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.length === 0 ? (
{colors.map((item, index) => ( <p className="mt-3 text-sm text-gray-400 italic">
<div key={item.id} className="space-y-3"> Tidak ada data warna
<Label className="text-sm text-gray-500"> </p>
Warna {index + 1} ) : (
</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>
{/* Preview warna */} {/* Preview warna */}
<div <div
className="w-12 h-12 rounded-full border" className="w-12 h-12 rounded-full border"
style={{ backgroundColor: item.colorSelected ?? "#e5e7eb" }} 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"
/> />
</div>
<p className="text-xs text-gray-500 text-center"> {/* Foto mobil */}
Foto Produk Warna {index + 1} <div className="w-full h-[90px] border rounded-lg overflow-hidden">
</p> <img
</div> src={item.preview}
))} alt={`warna-${index}`}
</div> 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>
<div> <div>
@ -557,29 +559,45 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
Spesifikasi Produk Spesifikasi Produk
</Label> </Label>
{specs.map((spec) => ( {specs.length === 0 ? (
<div <p className="mt-3 text-sm text-gray-400 italic">
key={spec.id} Tidak ada spesifikasi
className="mt-4 space-y-3 border-2 rounded-lg border-black p-3" </p>
> ) : (
<Input value={spec.title} readOnly className="font-medium" /> 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.length === 0 ? (
{spec.images.map((img, i) => ( <p className="text-sm text-gray-400 italic">
<div key={i} className="border rounded-lg overflow-hidden"> Tidak ada gambar spesifikasi
<Image </p>
src={img} ) : (
alt="spec" <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
width={200} {spec.images.map((img, i) => (
height={200} <div
className="object-cover w-full h-[120px]" 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> ))
))} )}
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold text-gray-700 mb-3"> <h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline Status Timeline

View File

@ -33,6 +33,7 @@ export default function UpdateProductForm() {
name: string; name: string;
preview: string; preview: string;
colorSelected: string | null; colorSelected: string | null;
isImageChanged: boolean;
}; };
const [colors, setColors] = useState<ColorType[]>([]); const [colors, setColors] = useState<ColorType[]>([]);
@ -43,6 +44,7 @@ export default function UpdateProductForm() {
const [price, setPrice] = useState<string>(""); const [price, setPrice] = useState<string>("");
const palette = [ const palette = [
"#000000",
"#1E4E52", "#1E4E52",
"#597E8D", "#597E8D",
"#6B6B6B", "#6B6B6B",
@ -78,61 +80,76 @@ export default function UpdateProductForm() {
name: "", name: "",
preview: "/car-default.png", preview: "/car-default.png",
colorSelected: null, colorSelected: null,
isImageChanged: false, // ✅ WAJIB
}, },
]); ]);
}; };
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [bannerFile, setBannerFile] = useState<File | null>(null);
const [uploadTarget, setUploadTarget] = useState<{ const [uploadTarget, setUploadTarget] = useState<{
type: "spec" | "color"; type: "spec" | "color" | "banner";
index: number; index?: number;
} | null>(null); } | null>(null);
const fileInputId = "file-upload-input"; const fileInputId = "file-upload-input";
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const input = e.target;
const file = input.files?.[0];
if (!file || !uploadTarget) return; if (!file || !uploadTarget) return;
const reader = new FileReader(); const previewUrl = URL.createObjectURL(file);
reader.onload = () => {
const fileUrl = reader.result as string;
if (uploadTarget.type === "spec") { // ===================== SPEC =====================
setSpecs((prev) => { if (uploadTarget.type === "spec") {
const updated = [...prev]; if (uploadTarget.index === undefined) return;
updated[uploadTarget.index].images.push(fileUrl);
return updated;
});
// Store the file for upload
if (file) {
setSpecFiles((prev) => {
const newMap = new Map(prev);
const existingFiles = newMap.get(uploadTarget.index) || [];
newMap.set(uploadTarget.index, [...existingFiles, file]);
return newMap;
});
}
}
if (uploadTarget.type === "color") { const index = uploadTarget.index;
setColors((prev) => {
const updated = [...prev];
updated[uploadTarget.index].preview = fileUrl;
return updated;
});
// Store the file for upload
if (file) {
setColorFiles((prev) => {
const newMap = new Map(prev);
newMap.set(uploadTarget.index, file);
return newMap;
});
}
}
};
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); setIsUploadDialogOpen(false);
}; };
@ -171,8 +188,9 @@ export default function UpdateProductForm() {
data.colors.map((color: any, index: number) => ({ data.colors.map((color: any, index: number) => ({
id: index + 1, id: index + 1,
name: color.name || "", name: color.name || "",
preview: color.image_url || data.thumbnail_url || "", preview: color.image_url || "",
colorSelected: color.name || null, colorSelected: color.name || null,
isImageChanged: false, // 🔥 default
})), })),
); );
} else { } else {
@ -221,15 +239,25 @@ export default function UpdateProductForm() {
})); }));
formData.append("colors", JSON.stringify(colorsPayload)); formData.append("colors", JSON.stringify(colorsPayload));
// Color images (only new files if uploaded) colors.forEach((color, index) => {
// Append files in order of color indices if (color.isImageChanged) {
colors.forEach((_, index) => { // image diganti → kirim file baru
const file = colorFiles.get(index); const file = colorFiles.get(index);
if (file) { if (file) {
formData.append("color_images", 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) // Specifications JSON (include image count for new files only)
const specificationsPayload = specs.map((s, index) => { const specificationsPayload = specs.map((s, index) => {
const newFiles = specFiles.get(index) || []; const newFiles = specFiles.get(index) || [];
@ -240,7 +268,6 @@ export default function UpdateProductForm() {
}); });
formData.append("specifications", JSON.stringify(specificationsPayload)); formData.append("specifications", JSON.stringify(specificationsPayload));
// Specification images (only new files if uploaded)
specs.forEach((_, index) => { specs.forEach((_, index) => {
const files = specFiles.get(index) || []; const files = specFiles.get(index) || [];
files.forEach((file) => { files.forEach((file) => {
@ -301,20 +328,33 @@ export default function UpdateProductForm() {
<div> <div>
<Label className="font-semibold">Thumbnail Produk</Label> <Label className="font-semibold">Thumbnail Produk</Label>
<div className="mt-2 w-[120px] h-[80px] rounded-lg overflow-hidden border bg-gray-100"> <div className="mt-2 flex items-center gap-4">
{thumbnail ? ( <div className="w-[120px] h-[80px] rounded-lg overflow-hidden border bg-gray-100">
<Image {thumbnail ? (
src={thumbnail} <Image
alt="Thumbnail" src={thumbnail}
width={120} alt="Thumbnail"
height={80} width={120}
className="object-cover" height={80}
/> className="object-cover"
) : ( />
<div className="flex items-center justify-center text-xs text-gray-400 h-full"> ) : (
No Image <div className="flex items-center justify-center text-xs text-gray-400 h-full">
</div> No Image
)} </div>
)}
</div>
<Button
type="button"
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "banner" });
setIsUploadDialogOpen(true);
}}
>
Upload Banner Baru
</Button>
</div> </div>
</div> </div>
@ -360,6 +400,7 @@ export default function UpdateProductForm() {
setColors((prev) => { setColors((prev) => {
const updated = [...prev]; const updated = [...prev];
updated[index].colorSelected = colorCode; updated[index].colorSelected = colorCode;
updated[index].name = colorCode; // ✅ sinkron ke input
return updated; return updated;
}); });
}} }}
@ -415,9 +456,16 @@ export default function UpdateProductForm() {
Judul Spesifikasi {index + 1} Judul Spesifikasi {index + 1}
</Label> </Label>
<Input <Input
defaultValue={spec.title} value={spec.title}
placeholder="Masukkan Judul Spesifikasi" placeholder="Masukkan Judul Spesifikasi"
className="mt-1" 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"> <Label className="font-semibold text-sm mt-4 block">
@ -426,14 +474,41 @@ export default function UpdateProductForm() {
<div className="flex flex-wrap gap-4 mt-2"> <div className="flex flex-wrap gap-4 mt-2">
{spec.images.map((img, i) => ( {spec.images.map((img, i) => (
<Image <div key={i} className="relative">
key={i} <Image
src={img} src={img}
width={120} width={120}
height={120} height={120}
alt="spec" alt="spec"
className="rounded-lg border object-cover" 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 <Button

View File

@ -2,36 +2,52 @@
import Image from "next/image"; import Image from "next/image";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { getAgentData } from "@/service/agent";
const agents = [ type Agent = {
{ id: number;
name: "Henny", name: string;
title: "Branch Manager Jaecoo Kelapa Gading", job_title: string;
image: "/henny.png", status_id: number;
}, profile_picture_url: string;
{ created_at: string;
name: "Zamroni", };
title: "Spv Jaecoo Kelapa Gading",
image: "/zamroni.png",
},
{
name: "Murtiyono",
title: "Spv Jaecoo Kelapa Gading",
image: "/murtiyono.jpg",
},
{
name: "Sutino",
title: "Spv Jaecoo Kelapa Gading",
image: "/sutino.png",
},
// {
// name: "Amendra Ismail",
// title: "Spv Jaecoo Kelapa Gading",
// image: "/amendra.png",
// },
];
export default function Agent() { 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(3, 7); // ✅ max 5
setAgents(latestApprovedAgents);
} catch (error) {
console.error("Failed to fetch agents:", error);
}
};
fetchAgents();
}, []);
return ( return (
<section className="py-16 px-6 md:px-5 bg-[#FAFDFF] text-center mt-0"> <section className="py-16 px-6 md:px-5 bg-[#FAFDFF] text-center mt-0">
<motion.h2 <motion.h2
@ -47,7 +63,7 @@ export default function Agent() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 place-items-center mt-10"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 place-items-center mt-10">
{agents.map((agent, index) => ( {agents.map((agent, index) => (
<motion.div <motion.div
key={index} key={agent.id}
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ transition={{
@ -56,19 +72,21 @@ export default function Agent() {
ease: "easeOut", ease: "easeOut",
}} }}
viewport={{ once: true, amount: 0.3 }} viewport={{ once: true, amount: 0.3 }}
className="bg-white shadow-md py-4 gap-2 flex flex-col items-center h-[300px] w-[250px]" className="bg-white shadow-md py-4 gap-2 flex flex-col items-center h-[300px] w-[250px]"
> >
<div className="relative w-44 h-48 mb-3"> <div className="relative w-44 h-48 mb-3">
<Image <Image
src={agent.image} src={agent.profile_picture_url}
alt={agent.name} alt={agent.name}
fill fill
className="rounded-full object-cover" className="rounded-full object-cover"
/> />
</div> </div>
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3> <h3 className="text-lg text-gray-900 text-center">{agent.name}</h3>
<p className="text-sm text-gray-600 text-center mt-1"> <p className="text-sm text-gray-600 text-center mt-1">
{agent.title} {agent.job_title} Kelapa Gading
</p> </p>
</motion.div> </motion.div>
))} ))}

View File

@ -2,42 +2,68 @@
import Image from "next/image"; import Image from "next/image";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { getAgentData } from "@/service/agent";
const agents = [ type Agent = {
{ id: number;
name: "Johny Nugroho", name: string;
title: "Branch Manager Jaecoo Cihampelas Bandung", job_title: string;
image: "/johny.png", status_id: number;
}, profile_picture_url: string;
{ created_at: string;
name: "Basuki Pamungkas", };
title: "Spv Jaecoo Cihampelas Bandung",
image: "/basuki.png",
},
{
name: "Deni Tihayar",
title: "Spv Jaecoo Cihampelas Bandung",
image: "/deni.png",
},
];
export default function BestAgent() { 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, 5); // ✅ max 5
setAgents(latestApprovedAgents);
} catch (error) {
console.error("Failed to fetch agents:", error);
}
};
fetchAgents();
}, []);
return ( 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 <motion.h2
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
viewport={{ once: true }} 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 Our Teams
</motion.h2> </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-4 gap-2 place-items-center mt-10">
{agents.map((agent, index) => ( {agents.map((agent, index) => (
<motion.div <motion.div
key={index} key={agent.id}
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
transition={{ transition={{
@ -46,19 +72,21 @@ export default function BestAgent() {
ease: "easeOut", ease: "easeOut",
}} }}
viewport={{ once: true, amount: 0.3 }} 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 <Image
src={agent.image} src={agent.profile_picture_url}
alt={agent.name} alt={agent.name}
fill fill
className="rounded-full object-cover" className="rounded-full object-cover"
/> />
</div> </div>
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3> <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> </p>
</motion.div> </motion.div>
))} ))}

View File

@ -1,4 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
export default function Footer() { export default function Footer() {
return ( return (
@ -6,79 +7,57 @@ export default function Footer() {
<div className="flex flex-col md:flex-row gap-10"> <div className="flex flex-col md:flex-row gap-10">
<div className="w-full md:w-4/12"> <div className="w-full md:w-4/12">
<Image <Image
src="/masjaecoo.png" src="/jaecoobot.png"
alt="Jaecoo" alt="Jaecoo"
width={300} width={300}
height={200} height={200}
className="ml-4" className="ml-4"
/> />
<div className="flex gap-4 mt-6 ml-24 md:ml-20 text-xl text-[#c7dbe3]"> <div className="flex gap-4 mt-6 ml-8 md:ml-8 text-xl text-[#c7dbe3]">
<div className="hover:text-white cursor-pointer"> <Link href={"https://www.instagram.com/jaecoo_kelapagading"}>
<svg <div className="hover:text-white cursor-pointer">
xmlns="http://www.w3.org/2000/svg" <svg
width="18" xmlns="http://www.w3.org/2000/svg"
height="18" width="18"
viewBox="0 0 24 24" height="18"
> viewBox="0 0 24 24"
<g >
fill="none" <g
stroke="currentColor" fill="none"
// stroke-width="1.5" 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_kelapagading"}>
<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 <path
// stroke-linecap="round" fill="currentColor"
// stroke-linejoin="round" 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"
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" /> </svg>
<path </div>
// stroke-linecap="round" </Link>
// 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>
</div> </div>
</div> </div>
<div className="md:w-8/12 "> <div className="md:w-8/12 ">
@ -130,10 +109,14 @@ export default function Footer() {
<h4 className="font-semibold text-white mb-4">CONTACT</h4> <h4 className="font-semibold text-white mb-4">CONTACT</h4>
<ul className="space-y-4 text-sm"> <ul className="space-y-4 text-sm">
<li> <li>
<a href="https://jaecoo.com" target="_blank" rel="noreferrer"> <a
jaecoo.com href="mailto:jaecookelapagading@gmail.com"
className="hover:underline"
>
jaecookelapagading@gmail.com
</a> </a>
</li> </li>
<li>0816 1124 631</li> <li>0816 1124 631</li>
<li> <li>
<p className="font-semibold text-white"> <p className="font-semibold text-white">

View File

@ -1,80 +1,127 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { ChevronLeft, ChevronRight } from "lucide-react"; import {
getAllGaleryFiles,
const imagesPerPage = 6; getGaleryData,
getGaleryFileData,
const galleryImages = [ } from "@/service/galery";
"/gl1.png",
"/gl-2-news.png",
"/gl3.png",
"/gl4.png",
"/gl5.png",
"/gl6.png",
"/gl7.png",
"/gl8.png",
"/gl9.png",
];
export default function GallerySection() { export default function GallerySection() {
const [currentPage, setCurrentPage] = useState(1); const [data, setData] = useState<any[]>([]);
const totalPages = Math.ceil(galleryImages.length / imagesPerPage); const [loading, setLoading] = useState(false);
const TABS = [
"All",
"Grand Opening",
"IIMS",
"GIIAS",
"GJAW",
"Exhibitions",
"Test Drive",
];
const paginatedImages = galleryImages.slice( const [activeTab, setActiveTab] = useState("All");
(currentPage - 1) * imagesPerPage,
currentPage * imagesPerPage 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 ( return (
<section className="py-16 px-4 max-w-[1400px] mx-auto"> <section className="py-16 px-4 max-w-[1400px] mx-auto">
<h2 className="text-4xl font-bold mb-8">Galeri Kami</h2> <h2 className="text-4xl font-bold mb-8">Galeri Kami</h2>
<div className="flex justify-center gap-2 mb-10">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> {TABS.map((tab) => (
{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) => (
<button <button
key={i} key={tab}
onClick={() => setCurrentPage(i + 1)} onClick={() => setActiveTab(tab)}
className={`w-8 h-8 rounded-md border text-sm ${ className={`px-4 py-2 rounded-full text-sm font-medium transition
currentPage === i + 1 ${
? "bg-[#1F6779] text-white" activeTab === tab
: "text-gray-700 hover:bg-gray-100" ? "bg-black text-white"
}`} : "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
> >
{i + 1} {tab}
</button> </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> </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> </section>
); );
} }

View File

@ -144,6 +144,41 @@ export default function HeaderAbout() {
/> />
</motion.div> </motion.div>
</div> </div>
<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> </section>
); );
} }

View File

@ -32,7 +32,7 @@ export default function HeaderPriceInformation() {
}, },
{ {
title: "JAECOO J5 EV", title: "JAECOO J5 EV",
image: "/j7-shs-nobg.png", image: "/price-j5.png",
price: "Rp 299.900.000", price: "Rp 299.900.000",
oldPrice: "Rp 299.900.000", oldPrice: "Rp 299.900.000",
capacity: "60.9kWh", capacity: "60.9kWh",
@ -45,7 +45,7 @@ export default function HeaderPriceInformation() {
{ {
title: "JAECOO J8 SHS-P ARDIS", title: "JAECOO J8 SHS-P ARDIS",
image: "/j8-awd-nobg.png", image: "/j8-awd-nobg.png",
price: "Rp 812.000.000", price: "Rp 828.000.000",
oldPrice: "Rp 828.000.000", oldPrice: "Rp 828.000.000",
capacity: "34,46kWh", capacity: "34,46kWh",
torque: "650Nm", torque: "650Nm",

View File

@ -14,30 +14,29 @@ import {
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { Download } from "lucide-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 [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0); const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false); const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o"; // const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`; // const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`; // const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`; // const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = [ const images = [
"/j5-putih.png", "/jj7-blue.png",
"/j5-hitam.png", "/jj7-white.png",
"/j5-silver.png", "/jj7-silver.png",
"/j5-biru.png", "/jj7-black.png",
"/j5-hijau.png",
]; ];
const gradients = [ const gradients = [
"linear-gradient(to bottom, #527D97, #527D97)",
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", "linear-gradient(to bottom, #FFFFFF, #FFFFFF)",
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)",
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", "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 ( return (
<> <>
@ -48,9 +47,9 @@ export default function HeaderProductJ7Awd() {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6" 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 <Image
src="/j5-ev.jpg" src="/shs-header.png"
alt="about-header" alt="about-header"
fill fill
className="object-cover" className="object-cover"
@ -59,76 +58,23 @@ export default function HeaderProductJ7Awd() {
/> />
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3"> <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}> <Link
<DialogTrigger asChild> href="https://cms.jaecoo.id/uploads/Flyer_J7_SHS_6db27c3a25.pdf"
<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"> target="_blank"
BROSUR rel="noopener noreferrer"
</Button> >
</DialogTrigger> <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"> <Link
<div className="flex justify-end p-4 bg-white z-50"> 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.`}
<a >
href={downloadLink} <Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
target="_blank" TEST DRIVE
rel="noopener noreferrer" </Button>
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3" </Link>
>
<Download size={18} />
</a>
</div>
<iframe
src={embedLink}
className="w-full h-[70vh] border-t"
allow="autoplay"
></iframe>
</DialogContent>
</Dialog>
<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>
<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>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -14,28 +14,31 @@ import {
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { Download } from "lucide-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 [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0); const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false); const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o"; // const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`; // const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`; // const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`; // const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = [ const images = [
"/jj7-blue.png", "/j5-putih.png",
"/jj7-white.png", "/j5-hitam.png",
"/jj7-silver.png", "/j5-silver.png",
"/jj7-black.png", "/j5-biru.png",
"/j5-hijau.png",
]; ];
const gradients = [ const gradients = [
"linear-gradient(to bottom, #527D97, #527D97)",
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", "linear-gradient(to bottom, #FFFFFF, #FFFFFF)",
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)",
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", "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 ( return (
<> <>
@ -46,9 +49,9 @@ export default function HeaderProductJ7Shs() {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6" 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 <Image
src="/shs-header.png" src="/j5-new1.jpg"
alt="about-header" alt="about-header"
fill fill
className="object-cover" className="object-cover"
@ -57,76 +60,23 @@ export default function HeaderProductJ7Shs() {
/> />
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3"> <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}> <Link
<DialogTrigger asChild> href="https://cms.jaecoo.id/uploads/Flyer_J5_EV_Ver3_smaller_file_size_d81b0f960c.pdf"
<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"> target="_blank"
BROSUR rel="noopener noreferrer"
</Button> >
</DialogTrigger> <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"> <Link
<div className="flex justify-end p-4 bg-white z-50"> 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.`}
<a >
href={downloadLink} <Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
target="_blank" TEST DRIVE
rel="noopener noreferrer" </Button>
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3" </Link>
>
<Download size={18} />
</a>
</div>
<iframe
src={embedLink}
className="w-full h-[70vh] border-t"
allow="autoplay"
></iframe>
</DialogContent>
</Dialog>
<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>
<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>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -14,17 +14,23 @@ import {
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import Link from "next/link";
export default function HeaderProductJ8Awd() { export default function HeaderProductJ8Awd() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0); const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false); const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o"; // const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`; // const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`; // const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`; // const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = ["/green.png", "/silver.png", "/white.png", "/black.png"]; const images = [
"/j8-green-ardis.png",
"/j8-silver-ardis.png",
"/j8-white-ardis.png",
"/j8-ardis-black.png",
];
const gradients = [ const gradients = [
"linear-gradient(to bottom, #527D97, #1F6779)", "linear-gradient(to bottom, #527D97, #1F6779)",
@ -52,7 +58,7 @@ export default function HeaderProductJ8Awd() {
/> />
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3"> <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> <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"> <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 BROSUR
@ -77,51 +83,23 @@ export default function HeaderProductJ8Awd() {
allow="autoplay" allow="autoplay"
></iframe> ></iframe>
</DialogContent> </DialogContent>
</Dialog> </Dialog> */}
<Link
<Dialog open={open} onOpenChange={setOpen}> href="https://cms.jaecoo.id/uploads/J8_SHS_ARDIS_Flyer_cbf280ea77.pdf"
<DialogTrigger asChild> target="_blank"
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer"> rel="noopener noreferrer"
TEST DRIVE >
</Button> <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">
</DialogTrigger> BROSUR
<DialogContent className="sm:max-w-[1400px] h-[600px]"> </Button>
<div className="flex items-center gap-4"> </Link>
<Image <Link
src="/masjaecoonav.png" 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.`}
alt="MAS JAECOO Logo" >
width={300} <Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
height={30} TEST DRIVE
className=" object-fill" </Button>
/> </Link>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
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>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@ -20,9 +20,10 @@ import {
} from "../ui/dialog"; } from "../ui/dialog";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import { useState } from "react"; import { useEffect, useState } from "react";
import Autoplay from "embla-carousel-autoplay"; // ✅ Import plugin autoplay import Autoplay from "embla-carousel-autoplay"; // ✅ Import plugin autoplay
import { useRef } from "react"; import { useRef } from "react";
import { getBannerData } from "@/service/banner";
const heroImages = [ const heroImages = [
"/Hero.png", "/Hero.png",
@ -32,126 +33,98 @@ const heroImages = [
"/Carousell-04.png", "/Carousell-04.png",
"/Carousell-05.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() { export default function Header() {
const [open, setOpen] = useState(false); 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 })); const plugin = useRef(Autoplay({ delay: 4000, stopOnInteraction: false }));
return ( return (
<section className="relative w-full overflow-hidden bg-white"> <section className="relative w-full overflow-hidden bg-white mt-5">
<Carousel <Carousel className="w-full relative" plugins={[plugin.current]}>
className="w-full relative"
plugins={[plugin.current]} // ✅ Tambahkan plugin di sini
>
<CarouselContent> <CarouselContent>
{heroImages.map((img, index) => ( {banners.map((banner, index) => (
<CarouselItem key={index}> <CarouselItem key={banner.id}>
<div className="relative w-full h-[400px] sm:h-[500px] md:h-[810px]"> <div className="relative w-full aspect-[16/9]">
<Image <Image
src={img} src={banner.thumbnail_url}
alt={`JAECOO Image ${index + 1}`} alt={banner.title}
width={1400} fill
height={810} priority={index === 0}
className="object-cover w-full h-full" className="object-contain"
/> />
{index === 0 && ( {/* {index === 0 && (
<div className="absolute inset-0 flex flex-col justify-center items-start px-4 sm:px-8 md:px-28 z-10"> <div className="absolute inset-0 flex flex-col justify-center items-start px-4 sm:px-8 md:px-28 z-10">
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.8 }}
className="text-2xl sm:text-3xl md:text-5xl font-bold text-black mb-4" className="text-2xl sm:text-3xl md:text-5xl font-bold text-black mb-4"
> >
JAECOO J7 SHS-P {banner.title}
</motion.h1> </motion.h1>
<motion.p {banner.description && (
initial={{ opacity: 0, y: 40 }} <motion.p
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 }} initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{ duration: 0.9, delay: 0.2 }}
duration: 1, className="text-sm sm:text-base md:text-lg text-black mb-6"
ease: "easeOut",
delay: 0.4,
}}
> >
<Dialog open={open} onOpenChange={setOpen}> {banner.description}
<DialogTrigger asChild> </motion.p>
<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>
)} )} */}
</div> </div>
</CarouselItem> </CarouselItem>
))} ))}

View File

@ -5,10 +5,9 @@ import { useInView } from "react-intersection-observer";
const featuresInt = [ const featuresInt = [
{ {
title: "14.8 Screen with APPLE Carplay & Android Auto", title: "13.2 Screen with APPLE Carplay & Android Auto",
description: description: "13.2 Full HD Center Display",
"Stay connected and informed with a 14.8 display offering clear visuals and advanced functionality for a seamless driving experience.", image: "/headunit.png",
image: "/in-shs2.png",
}, },
{ {
title: "Horizontal Side by Side Cup Holder", title: "Horizontal Side by Side Cup Holder",
@ -24,9 +23,8 @@ const featuresInt = [
}, },
{ {
title: "Wireless Charging", title: "Wireless Charging",
description: description: "Fast Wireless Charging 50W",
"Stay powered up on the go with Wireless Charging, ensuring your devices are always ready when you are.", image: "/charging.png",
image: "/in-shs5.png",
}, },
]; ];
@ -78,7 +76,7 @@ export default function InteriorShs() {
transition={{ duration: 0.7 }} transition={{ duration: 0.7 }}
> >
<Image <Image
src="/in-shs.png" src="/inter.png"
alt="Interior Hero" alt="Interior Hero"
fill fill
className="object-cover" className="object-cover"

View File

@ -20,13 +20,13 @@ const items = [
image: "/new-car2.png", image: "/new-car2.png",
title: "JAECOO J7 SHS-P", title: "JAECOO J7 SHS-P",
description: "DELICATE OFF-ROAD SUV", 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", title: "JAECOO J5 EV",
description: "SUPER HYBRID SYSTEM = SUPER HEV + EV", description: "SUPER HYBRID SYSTEM = SUPER HEV + EV",
link: "/product/j7-shs", link: "/product/j5-ev",
}, },
{ {
image: "/new-car3.png", image: "/new-car3.png",
@ -87,7 +87,7 @@ export default function Items() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.25 + 0.6, duration: 0.6 }} transition={{ delay: index * 0.25 + 0.6, duration: 0.6 }}
> >
<Dialog open={open} onOpenChange={setOpen}> {/* <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer"> <Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
TEST DRIVE TEST DRIVE
@ -109,7 +109,7 @@ export default function Items() {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" /> <Input placeholder="Nama" />
<Input placeholder="Email" /> <Input placeholder="Email" />
@ -130,7 +130,15 @@ export default function Items() {
</Button> </Button>
</div> </div>
</DialogContent> </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}> <Link href={item?.link}>
<Button <Button
variant="outline" variant="outline"

View File

@ -115,7 +115,7 @@ export default function Navbar() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Link href="/" className="flex items-center space-x-2"> <Link href="/" className="flex items-center space-x-2">
<Image <Image
src="/masjaecoonav.png" src="/jaecoonew.png"
alt="MAS JAECOO Logo" alt="MAS JAECOO Logo"
width={300} width={300}
height={30} height={30}
@ -195,50 +195,13 @@ export default function Navbar() {
/> />
<p className="font-bold mt-4 text-center">{car.name}</p> <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"> <div className="flex flex-col sm:flex-row gap-2 mt-2 items-center">
<Dialog open={open} onOpenChange={setOpen}> <Link
<DialogTrigger asChild> 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:w-[200px] md:h-[40px] rounded-full hover:cursor-pointer"> >
TEST DRIVE <Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
</Button> TEST DRIVE
</DialogTrigger> </Button>
<DialogContent className="sm:max-w-[1400px] h-[600px]"> </Link>
<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={car.link} className="w-[200px]"> <Link href={car.link} className="w-[200px]">
<Button <Button
variant="outline" variant="outline"

View File

@ -2,151 +2,124 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight } from "lucide-react";
const tabs = ["INSTAGRAM", "TIKTOK", "FACEBOOK", "YOUTUBE"]; const tabs = ["INSTAGRAM", "TIKTOK"];
const instagramPosts = ["/ig-news1.png", "/ig-news2.png", "/ig-news3.png"]; const instagramPosts = ["/Asset19.png", "/Asset20.png", "/isra.png"];
const tiktokPosts = ["/tk-news1.png", "/tk-news2.png", "/tk-news3.png"];
const youtubePosts = ["/tk-news1.png", "/tk-news2.png", "/tk-news3.png"]; const tiktokPosts = ["/tiktok1.jpeg", "/tiktok2.jpeg", "/tiktok3.jpeg"];
const facebookPosts = ["/tk-news1.png", "/tk-news2.png", "/tk-news3.png"];
export default function SosmedSection() { export default function SosmedSection() {
const [activeTab, setActiveTab] = useState("INSTAGRAM"); const [activeTab, setActiveTab] = useState("INSTAGRAM");
return ( return (
<section className="px-4 py-16 max-w-[1400px] mx-auto"> <section className="px-4 py-16 max-w-[1400px] mx-auto">
<h2 className="text-3xl font-bold mb-6 text-start ml-16"> {/* Title */}
Sosial Media Kami <h2 className="text-3xl font-bold mb-2 ml-4">Sosial Media Kami</h2>
</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) => ( {tabs.map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`text-sm font-medium px-4 py-2 rounded-full ${ className={`px-5 py-2 rounded-full text-sm font-medium transition
activeTab === tab ${
? "bg-[#BCD4DF] text-sky-700" activeTab === tab
: "text-[gray-700] hover:bg-gray-100" ? "bg-[#BCD4DF] text-sky-700"
}`} : "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
> >
{tab} {tab}
</button> </button>
))} ))}
</div> </div>
{/* INSTAGRAM */}
{activeTab === "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) => ( {instagramPosts.map((img, i) => (
<div <Link
key={i} 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/jaecoo_kelapagading"
target="_blank"
> >
<Image <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">
src={img} <Image
alt={`Instagram post ${i + 1}`} src={img}
fill alt={`Instagram post ${i + 1}`}
className="w-full h-full object-cover" fill
/> className="object-cover transition-transform duration-300 group-hover:scale-105"
</div> />
{/* 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>
<div className="flex justify-center mt-10"> <div className="flex justify-center mt-12">
<Link href={"https://www.instagram.com/jaecoo_kelapagading"}> <Link
href="https://www.instagram.com/jaecoo_kelapagading"
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"> <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 Lihat Selengkapnya
<ArrowRight size={35} /> <ArrowRight size={28} />
</button> </button>
</Link> </Link>
</div> </div>
</> </>
)} )}
{/* TIKTOK */}
{activeTab === "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) => ( {tiktokPosts.map((img, i) => (
<div <Link
key={i} 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_kelapagading"
target="_blank"
> >
<Image <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">
src={img} <Image
alt={`Tiktok post ${i + 1}`} src={img}
fill alt={`TikTok post ${i + 1}`}
className="w-full h-full object-cover" fill
/> className="object-cover transition-transform duration-300 group-hover:scale-105"
</div> />
{/* 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>
<div className="flex justify-center mt-10"> <div className="flex justify-center mt-12">
<Link href={"https://www.tiktok.com/@jaecoo_kelapagading"}> <Link
href="https://www.tiktok.com/@jaecoo_kelapagading"
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"> <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 Lihat Selengkapnya
<ArrowRight size={35} /> <ArrowRight size={28} />
</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} />
</button> </button>
</Link> </Link>
</div> </div>

View File

@ -1,16 +1,56 @@
"use client"; "use client";
import { getBannerData } from "@/service/banner";
import Image from "next/image"; 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() { 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 ( return (
<section className="pt-10 bg-white"> <section className="pt-10 bg-white">
<div className="relative mb-10 w-full h-[250px] sm:h-[500px] md:h-[700px]"> <div className="relative mb-10 w-full aspect-[16/9]">
<Image <Image
src={"/maintenance.png"} src={banner?.thumbnail_url || "/maintenance.png"}
alt="maintenance" alt={banner?.title || "Banner"}
fill fill
className="object-cover" className="object-contain"
priority priority
/> />
</div> </div>

View File

@ -56,6 +56,7 @@ import {
approvePromotion, approvePromotion,
rejectPromotion, rejectPromotion,
} from "@/service/promotion"; } from "@/service/promotion";
import PromoEditDialog from "../dialog/promo-edit-dialog";
const columns = [ const columns = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
@ -95,6 +96,11 @@ export default function PromotionTable() {
const [categories, setCategories] = useState<any>([]); const [categories, setCategories] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>(""); const [selectedCategories, setSelectedCategories] = useState<any>("");
const [openDetail, setOpenDetail] = useState(false); const [openDetail, setOpenDetail] = useState(false);
const [openEditPromo, setOpenEditPromo] = useState(false);
const [selectedEditPromoId, setSelectedEditPromoId] = useState<number | null>(
null,
);
const [selectedPromo, setPromoDetail] = useState<any>(null); const [selectedPromo, setPromoDetail] = useState<any>(null);
const [selectedPromoId, setSelectedPromoId] = useState<number | null>(null); const [selectedPromoId, setSelectedPromoId] = useState<number | null>(null);
const [startDateValue, setStartDateValue] = useState({ const [startDateValue, setStartDateValue] = useState({
@ -474,7 +480,19 @@ export default function PromotionTable() {
<Eye className="w-4 h-4 mr-1" /> Lihat <Eye className="w-4 h-4 mr-1" /> Lihat
</Button> </Button>
{/* Tombol Edit */} {/* 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 */} {/* Tombol Approve & Reject - hanya untuk admin dan status pending */}
{/* {userRoleId === "1" && item.status_id === 1 && ( {/* {userRoleId === "1" && item.status_id === 1 && (
<> <>
@ -529,6 +547,15 @@ export default function PromotionTable() {
open={openDetail} open={openDetail}
onOpenChange={setOpenDetail} onOpenChange={setOpenDetail}
/> />
<PromoEditDialog
promoId={selectedEditPromoId}
open={openEditPromo}
onOpenChange={setOpenEditPromo}
onSuccess={() => {
setOpenEditPromo(false);
initState(); // refresh table setelah edit
}}
/>
{/* FOOTER PAGINATION */} {/* FOOTER PAGINATION */}
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600"> <div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">

View File

@ -1,17 +1,24 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
images: { images: {
domains: [ remotePatterns: [
"mikulnews.com", {
"dev.mikulnews.com", protocol: "https",
"jaecoocihampelasbdg.com", hostname: "jaecoocihampelasbdg.com",
"jaecookelapagading.com", pathname: "/api/**",
},
{
protocol: "https",
hostname: "jaecookelapagading.com",
pathname: "/api/**",
},
], ],
}, },
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
}, },
experimental: { experimental: {
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"], optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
}, },

3
package-lock.json generated
View File

@ -3232,6 +3232,7 @@
"version": "19.2.7", "version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -3240,7 +3241,7 @@
"version": "19.2.3", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "dev": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }

BIN
public/Asset18.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
public/Asset19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

BIN
public/Asset20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
public/Asset21.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

BIN
public/charging.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
public/front.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -0,0 +1 @@
google-site-verification: google162462b69256f396.html

View File

@ -1 +0,0 @@
google-site-verification: googlede15e50f58ee7487.html

BIN
public/headunit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

BIN
public/inter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

BIN
public/isra.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

BIN
public/j5-ev-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/j5-new1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

BIN
public/j8-ardis-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
public/j8-green-ardis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

BIN
public/j8-silver-ardis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
public/j8-white-ardis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
public/jaecoobot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/jaecoonew.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/panoramic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
public/price-j5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

BIN
public/rear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
public/tiktok1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

BIN
public/tiktok2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
public/tiktok3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

View File

@ -17,6 +17,10 @@ export async function getGaleryFileData(props: PaginationRequest) {
return await httpGetInterceptor(`/gallery-files`); return await httpGetInterceptor(`/gallery-files`);
} }
export async function getAllGaleryFiles() {
return await httpGetInterceptor(`/gallery-files`);
}
export async function getGaleryById(id: any) { export async function getGaleryById(id: any) {
const headers = { const headers = {
"content-type": "application/json", "content-type": "application/json",

View File

@ -1,8 +1,10 @@
import axios from "axios"; import axios from "axios";
// Mengambil base URL dari environment variable, default ke API production // Mengambil base URL dari environment variable, default ke API production
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api"; const baseURL =
const clientKey = process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"; process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api";
const clientKey =
process.env.NEXT_PUBLIC_CLIENT_KEY || "9f83220f-88d7-433f-98cc-fc5ef5a16ab9";
const axiosBaseInstance = axios.create({ const axiosBaseInstance = axios.create({
baseURL, baseURL,

View File

@ -3,8 +3,10 @@ import { postSignIn } from "../master-user";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
// Mengambil base URL dari environment variable, default ke API production // Mengambil base URL dari environment variable, default ke API production
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api"; const baseURL =
const clientKey = process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"; process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api";
const clientKey =
process.env.NEXT_PUBLIC_CLIENT_KEY || "9f83220f-88d7-433f-98cc-fc5ef5a16ab9";
const refreshToken = Cookies.get("refresh_token"); const refreshToken = Cookies.get("refresh_token");
@ -30,7 +32,7 @@ axiosInterceptorInstance.interceptors.request.use(
}, },
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error);
} },
); );
// Response interceptor // Response interceptor
@ -68,7 +70,7 @@ axiosInterceptorInstance.interceptors.response.use(
} }
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default axiosInterceptorInstance; export default axiosInterceptorInstance;

View File

@ -2,11 +2,11 @@ import axiosBaseInstance from "./axios-base-instance";
const defaultHeaders = { const defaultHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640" "X-Client-Key": "9f83220f-88d7-433f-98cc-fc5ef5a16ab9",
}; };
export async function httpGet(pathUrl: any, headers?: any) { export async function httpGet(pathUrl: any, headers?: any) {
console.log("X-HEADERS : ", defaultHeaders) console.log("X-HEADERS : ", defaultHeaders);
const mergedHeaders = { const mergedHeaders = {
...defaultHeaders, ...defaultHeaders,
...headers, ...headers,

View File

@ -5,11 +5,11 @@ import { getCsrfToken } from "../master-user";
const defaultHeaders = { const defaultHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640" "X-Client-Key": "9f83220f-88d7-433f-98cc-fc5ef5a16ab9",
}; };
export async function httpGetInterceptor(pathUrl: any) { export async function httpGetInterceptor(pathUrl: any) {
console.log("X-HEADERS : ", defaultHeaders) console.log("X-HEADERS : ", defaultHeaders);
const response = await axiosInterceptorInstance const response = await axiosInterceptorInstance
.get(pathUrl, { headers: defaultHeaders }) .get(pathUrl, { headers: defaultHeaders })
.catch((error) => error.response); .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 resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.csrf_token; 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 resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.csrf_token; 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) { export async function httpDeleteInterceptor(pathUrl: any, headers?: any) {
const resCsrf = await getCsrfToken(); const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.csrf_token; const csrfToken = resCsrf?.data?.csrf_token;
const mergedHeaders = { const mergedHeaders = {