Initial commit

This commit is contained in:
Anang Yusman 2025-07-13 15:48:15 +08:00
commit 9116abc611
1177 changed files with 934223 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,260 @@
"use client";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import AdvertiseTable from "@/components/table/advertise/advertise-table";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import { close, error, loading } from "@/config/swal";
import useDisclosure from "@/components/useDisclosure";
import { createAdvertise } from "@/service/advertisement";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
url: z.string().min(1, {
message: "Link harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
});
export default function AdvertisePage() {
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const MySwal = withReactContent(Swal);
const [refresh, setRefresh] = useState(false);
const [placement, setPlacement] = useState("banner");
const [files, setFiles] = useState<File[]>([]);
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", url: "" },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
accept:
placement === "banner"
? {
"image/*": [],
"video/*": [],
}
: { "image/*": [] },
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
title: values.title,
description: values.description,
placement: placement,
redirectLink: values.url,
};
const res = await createAdvertise(formData);
if (res?.error) {
error(res?.message);
return false;
}
// const idNow = res?.data?.data?.id;
if (files.length > 0) {
const formFiles = new FormData();
formFiles.append("file", files[0]);
// const resFile = await createMediaFileAdvertise(idNow, formFiles);
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
setRefresh(!refresh);
}
});
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<Button
size="default"
className="bg-[#F07C00] text-white w-full lg:w-fit"
onClick={onOpen}
>
Buat Baru
<AddIcon />
</Button>
<AdvertiseTable triggerRefresh={refresh} />
</div>
</div>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Advertise</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Judul</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
id="title"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm">{errors.title?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<Textarea
id="description"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Link</p>
<Controller
control={control}
name="url"
render={({ field: { onChange, value } }) => (
<Input
id="url"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.url && (
<p className="text-red-400 text-sm">{errors.url?.message}</p>
)}
</div>
<p className="text-sm mt-3">Penempatan</p>
<RadioGroup
value={placement}
onValueChange={setPlacement}
className="flex flex-row gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="banner" id="banner" />
<Label htmlFor="banner">Banner</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="jumbotron" id="jumbotron" />
<Label htmlFor="jumbotron">Jumbotron</Label>
</div>
</RadioGroup>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>
{files.length < 1 && (
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex items-center flex-col">
<CloudUploadIcon />
<h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className="text-xs text-muted-foreground">
(Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
maksimal 100mb.)
</div>
</div>
</div>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<img
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
variant="outline"
size="icon"
onClick={() => handleRemoveFile(files[0])}
>
<TimesIcon />
</Button>
</div>
)}
</div>
<DialogFooter className="pt-4">
<Button type="submit">Simpan</Button>
<Button variant="ghost" onClick={onClose}>
Tutup
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,9 @@
import CreateArticleForm from "@/components/form/article/create-article-form";
export default function CreateArticle() {
return (
<div className="bg-slate-100 p-3 lg:p-8 dark:!bg-black overflow-y-auto">
<CreateArticleForm />
</div>
);
}

View File

@ -0,0 +1,22 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function DetailArticlePage() {
return (
<div className="">
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
<div className="flex flex-col gap-1 py-2">
<h1 className="font-bold text-[25px]">Article</h1>
<p className="text-[14px]">Article</p>
</div>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
</svg>
</span>
</div> */}
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={true} />
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function UpdateArticlePage() {
return (
<div>
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
<div className="flex flex-col gap-1 py-2">
<h1 className="font-bold text-[25px]">Article</h1>
<p className="text-[14px]">Article</p>
</div>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
</svg>
</span>
</div> */}
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={false} />
</div>
</div>
);
}

View File

@ -0,0 +1,36 @@
"use client";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import Link from "next/link";
export default function BasicPage() {
return (
<div>
{/* <div className="flex flex-row justify-between border-b-2 mb-4 px-4 ">
<div className="flex flex-col gap-1 py-2">
<h1 className="font-bold text-[25px]">Article</h1>
<p className="text-[14px]">Article</p>
</div>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
</svg>
</span>
</div> */}
<div className="overflow-x-hidden overflow-y-scroll w-full">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<Link href="/admin/article/create">
<Button className="bg-[#F07C00] text-white w-full lg:w-fit hover:bg-[#d96e00]">
Tambah Artikel
<Plus className="ml-2 h-4 w-4" />
</Button>
</Link>
<ArticleTable />
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
"use client";
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
export default function AdminPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50 flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<motion.div
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="p-6">
<DashboardContainer />
</div>
</motion.div>
);
}

View File

@ -0,0 +1,7 @@
"use client";
import { AdminLayout } from "@/components/layout/admin-layout";
export default function AdminPageLayout({ children }: { children: React.ReactNode }) {
return <AdminLayout>{children}</AdminLayout>;
}

View File

@ -0,0 +1,24 @@
"use client";
import MagazineTable from "@/components/table/magazine/magazine-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import Link from "next/link";
export default function MagazineTablePage() {
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3 w-full">
<Link href="/admin/magazine/create">
<Button className="bg-[#F07C00] text-white hover:bg-[#d96e00] w-full lg:w-auto">
<Plus className="ml-2 h-4 w-4" />
Tambah Majalah
</Button>
</Link>
<MagazineTable />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,341 @@
"use client";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import CategoriesTable from "@/components/table/master-categories/categories-table";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { useDropzone } from "react-dropzone";
import { close, error, loading } from "@/config/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Image from "next/image";
import { getArticleByCategory } from "@/service/article";
import {
createCategory,
uploadCategoryThumbnail,
} from "@/service/master-categories";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import useDisclosure from "@/components/useDisclosure";
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
tags: z.array(z.string()),
// parent: z.array(categorySchema).nonempty({
// message: "Kategori harus memiliki setidaknya satu item",
// }),
// tags: z.array(z.string()).nonempty({
// message: "Minimal 1 tag",
// }),
});
interface CategoryType {
id: number;
label: string;
value: number;
}
export default function MasterCategoryTable() {
const MySwal = withReactContent(Swal);
const animatedComponents = makeAnimated();
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [refresh, setRefresh] = useState(false);
const [tag, setTag] = useState("");
const [selectedParent, setSelectedParent] = useState<any>();
const [isDetail] = useState<any>();
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", tags: [] },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
fetchCategory();
}, []);
const fetchCategory = async () => {
const res = await getArticleByCategory();
if (res?.data?.data) {
setupCategory(res?.data?.data);
}
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
setListCategory(temp);
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
console.log("values,", values);
loading();
const formData = {
title: values.title,
statusId: 1,
parentId: selectedParent ? selectedParent.id : 0,
tags: values.tags.join(","),
description: values.description,
};
const response = await createCategory(formData);
if (response?.error) {
error(response.message);
return false;
}
const categoryId = response?.data?.data?.id;
const formFiles = new FormData();
formFiles.append("files", files[0]);
const resFile = await uploadCategoryThumbnail(categoryId, formFiles);
if (resFile?.error) {
error(resFile.message);
return false;
}
close();
setRefresh(!refresh);
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<Button
size="default"
className="bg-[#F07C00] text-white w-full lg:w-fit flex items-center gap-2"
onClick={onOpen}
>
Tambah Kategori
<AddIcon />
</Button>
<CategoriesTable triggerRefresh={refresh} />
</div>
</div>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl">
<DialogHeader>
<DialogTitle>Kategori Baru</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Nama Kategori</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
id="title"
value={value}
onChange={onChange}
readOnly={isDetail}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm">{errors.title?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<Textarea
id="description"
value={value}
onChange={onChange}
readOnly={isDetail}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Parent</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: () =>
"!rounded-lg bg-white !border !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={selectedParent}
isDisabled={isDetail}
onChange={setSelectedParent}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable
isSearchable
isMulti={false}
placeholder="Kategori..."
name="sub-module"
options={listCategory}
/>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Tag Terkait</p>
<Controller
control={control}
name="tags"
render={({ field: { value } }) => (
<Input
id="tags"
value={tag}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
className="w-full border rounded-lg"
/>
)}
/>
<div className="flex flex-wrap gap-1 mt-2">
{getValues("tags")?.map((item, index) => (
<div
key={index}
className="bg-blue-500 text-white px-2 py-1 rounded-full text-sm flex items-center gap-1"
>
{item}
<button
type="button"
onClick={() => {
const filteredTags = getValues("tags").filter(
(tag) => tag !== item
);
if (filteredTags.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue("tags", filteredTags);
}
}}
>
&times;
</button>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>
{files.length < 1 && (
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex flex-col items-center">
<CloudUploadIcon />
<h4 className="text-2xl font-medium mb-1 mt-3 text-muted-foreground">
Tarik file disini atau klik untuk upload.
</h4>
<div className="text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
maksimal 100mb.)
</div>
</div>
</div>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<Image
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
width={480}
height={480}
/>
<Button
variant="outline"
onClick={() => handleRemoveFile(files[0])}
>
<TimesIcon />
</Button>
</div>
)}
</div>
<DialogFooter className="self-end">
{!isDetail && <Button type="submit">Simpan</Button>}
<Button variant="outline" onClick={onClose}>
Tutup
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,10 @@
import FormMasterUser from "@/components/form/form-master-user";
import { Card } from "@/components/ui/card";
export default function CreateMasterUserPage() {
return (
<Card className="h-[96vh] rounded-md bg-transparent">
<FormMasterUser />
</Card>
);
}

View File

@ -0,0 +1,10 @@
import FormMasterUserEdit from "@/components/form/form-master-user-edit";
import { Card } from "@/components/ui/card";
export default function CreateMasterUserPage() {
return (
<Card className="h-[96vh] rounded-md bg-transparent">
<FormMasterUserEdit />
</Card>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import { AddIcon } from "@/components/icons";
import MasterUserTable from "@/components/table/master-user-table";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function MasterUserPage() {
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl py-3">
<Link href="/admin/master-user/create" className="mx-3">
<Button
size="default"
color="primary"
className="bg-[#F07C00] text-white"
>
Pengguna Baru
<AddIcon />
</Button>
</Link>
<MasterUserTable />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import StaticPageBuilder from "@/components/main/static-page/static-page-main";
export default function StaticPageGenerator() {
return (
<div className="bg-transparent p-4 overflow-auto">
<StaticPageBuilder />
</div>
);
}

View File

@ -0,0 +1,10 @@
import StaticPageBuilderEdit from "@/components/form/static-page/static-page-edit-form";
import { Card } from "@/components/ui/card";
export default function StaticPageEdit() {
return (
<Card className="rounded-md bg-transparent p-4 overflow-auto">
<StaticPageBuilderEdit />
</Card>
);
}

View File

@ -0,0 +1,22 @@
import { AddIcon } from "@/components/icons";
import StaticPageTable from "@/components/table/static-page-table";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function StaticPageGeneratorList() {
return (
<div className="overflow-x-hidden overflow-y-scroll rounded-lg">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<Link href="/admin/static-page/create">
<Button size="default" color="primary" className="bg-[#F07C00] text-white w-full lg:w-fit">
Tambah Halaman
<AddIcon />
</Button>
</Link>
<StaticPageTable />
</div>
</div>
</div>
);
}

26
app/about/galery/page.tsx Normal file
View File

@ -0,0 +1,26 @@
import Agent from "@/components/landing-page/agent";
import BestAgent from "@/components/landing-page/best-agent";
import Footer from "@/components/landing-page/footer";
import Galeri from "@/components/landing-page/galeri";
import GallerySection from "@/components/landing-page/galery";
import HeaderAbout from "@/components/landing-page/header-about";
import HeaderItems from "@/components/landing-page/header-item";
import Help from "@/components/landing-page/help";
import FormJaecoo from "@/components/landing-page/jaecoo-form";
import Navbar from "@/components/landing-page/navbar";
import NearestLocation from "@/components/landing-page/nearest-location";
import Service from "@/components/landing-page/service";
export default function AboutPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<GallerySection />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
import Agent from "@/components/landing-page/agent";
import BestAgent from "@/components/landing-page/best-agent";
import Footer from "@/components/landing-page/footer";
import Galeri from "@/components/landing-page/galeri";
import HeaderAbout from "@/components/landing-page/header-about";
import HeaderItems from "@/components/landing-page/header-item";
import Help from "@/components/landing-page/help";
import FormJaecoo from "@/components/landing-page/jaecoo-form";
import Navbar from "@/components/landing-page/navbar";
import NearestLocation from "@/components/landing-page/nearest-location";
import Service from "@/components/landing-page/service";
export default function AboutPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderAbout />
<BestAgent />
<Footer />
</div>
</div>
);
}

27
app/about/sosmed/page.tsx Normal file
View File

@ -0,0 +1,27 @@
import Agent from "@/components/landing-page/agent";
import BestAgent from "@/components/landing-page/best-agent";
import Footer from "@/components/landing-page/footer";
import Galeri from "@/components/landing-page/galeri";
import GallerySection from "@/components/landing-page/galery";
import HeaderAbout from "@/components/landing-page/header-about";
import HeaderItems from "@/components/landing-page/header-item";
import Help from "@/components/landing-page/help";
import FormJaecoo from "@/components/landing-page/jaecoo-form";
import Navbar from "@/components/landing-page/navbar";
import NearestLocation from "@/components/landing-page/nearest-location";
import Service from "@/components/landing-page/service";
import SosmedSection from "@/components/landing-page/social-media";
export default function AboutPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<SosmedSection />
<Footer />
</div>
</div>
);
}

7
app/auth/layout.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return <> {children}</>;
}

10
app/auth/page.tsx Normal file
View File

@ -0,0 +1,10 @@
import Login from "@/components/form/login";
import React from "react";
export default function AuthPage() {
return (
<>
<Login />
</>
);
}

View File

@ -0,0 +1,17 @@
import Footer from "@/components/landing-page/footer";
import HeaderAfterSalesServices from "@/components/landing-page/header-after-sales";
import HeaderPriceInformation from "@/components/landing-page/header-price";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import Navbar from "@/components/landing-page/navbar";
export default function AfterSalesServicesPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderAfterSalesServices />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
import Footer from "@/components/landing-page/footer";
import HeaderAfterSalesServices from "@/components/landing-page/header-after-sales";
import HeaderPriceInformation from "@/components/landing-page/header-price";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import HeaderSalesServices from "@/components/landing-page/header-sales";
import Navbar from "@/components/landing-page/navbar";
export default function SalesServicesPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderSalesServices />
<Footer />
</div>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

122
app/globals.css Normal file
View File

@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

30
app/layout.tsx Normal file
View File

@ -0,0 +1,30 @@
import type { Metadata } from "next";
// import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
// const geistSans = Geist({
// variable: "--font-geist-sans",
// subsets: ["latin"],
// });
// const geistMono = Geist_Mono({
// variable: "--font-geist-mono",
// subsets: ["latin"],
// });
export const metadata: Metadata = {
title: "Jaecoo",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

25
app/page.tsx Normal file
View File

@ -0,0 +1,25 @@
import Footer from "@/components/landing-page/footer";
import Header from "@/components/landing-page/header";
import Items from "@/components/landing-page/items";
import Location from "@/components/landing-page/location";
import Navbar from "@/components/landing-page/navbar";
import Video from "@/components/landing-page/video";
import Agent from "@/components/landing-page/agent";
export default function Home() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<div className="flex-1">
<Header />
</div>
<Items />
<Video />
<Agent />
{/* <Location /> */}
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,16 @@
import Footer from "@/components/landing-page/footer";
import HeaderPriceInformation from "@/components/landing-page/header-price";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import Navbar from "@/components/landing-page/navbar";
export default function PriceInformationPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderPriceInformation />
<Footer />
</div>
</div>
);
}

15
app/price/promo/page.tsx Normal file
View File

@ -0,0 +1,15 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
import HeaderPromo from "@/components/landing-page/promo";
export default function PromoPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderPromo />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import Exterior from "@/components/landing-page/exterior";
import FeaturesAndSpecifications from "@/components/landing-page/features-and-specifications";
import Footer from "@/components/landing-page/footer";
import HeaderProductJ7Awd from "@/components/landing-page/header-product-j7-awd";
import Interior from "@/components/landing-page/interior";
import Navbar from "@/components/landing-page/navbar";
export default function ProductJ7Page() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderProductJ7Awd />
<Exterior />
<Interior />
<FeaturesAndSpecifications />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import ExteriorShs from "@/components/landing-page/exterior-shs";
import FeaturesAndSpecificationsShs from "@/components/landing-page/features-and-specifications-shs";
import Footer from "@/components/landing-page/footer";
import HeaderProductJ7Shs from "@/components/landing-page/header-product-j7-shs";
import InteriorShs from "@/components/landing-page/interior-shs";
import Navbar from "@/components/landing-page/navbar";
export default function ProductJ7ShsPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderProductJ7Shs />
<ExteriorShs />
<InteriorShs />
<FeaturesAndSpecificationsShs />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import ExteriorJ8Awd from "@/components/landing-page/exterior-j8-awd";
import FeaturesAndSpecificationsJ8 from "@/components/landing-page/features-and-specifications-j8";
import Footer from "@/components/landing-page/footer";
import HeaderProductJ8Awd from "@/components/landing-page/header-product-j8-awd";
import InteriorJ8Awd from "@/components/landing-page/interior-j8-awd";
import Navbar from "@/components/landing-page/navbar";
export default function ProductJ8Page() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderProductJ8Awd />
<ExteriorJ8Awd />
<InteriorJ8Awd />
<FeaturesAndSpecificationsJ8 />
<Footer />
</div>
</div>
);
}

21
app/product/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import Exterior from "@/components/landing-page/exterior";
import FeaturesAndSpecifications from "@/components/landing-page/features-and-specifications";
import Footer from "@/components/landing-page/footer";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import Interior from "@/components/landing-page/interior";
import Navbar from "@/components/landing-page/navbar";
export default function ProductPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderProduct />
<Exterior />
<Interior />
<FeaturesAndSpecifications />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
import HeaderAfterSales from "@/components/landing-page/after-sales";
import Footer from "@/components/landing-page/footer";
import HeaderPriceInformation from "@/components/landing-page/header-price";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import Navbar from "@/components/landing-page/navbar";
import HeaderProgramSales from "@/components/landing-page/program-sales";
import HeaderPromo from "@/components/landing-page/promo";
export default function AfterSalesPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderAfterSales />
<Footer />
</div>
</div>
);
}

20
app/service/page.tsx Normal file
View File

@ -0,0 +1,20 @@
import Footer from "@/components/landing-page/footer";
import HeaderItems from "@/components/landing-page/header-item";
import Navbar from "@/components/landing-page/navbar";
import NearestLocation from "@/components/landing-page/nearest-location";
import Service from "@/components/landing-page/service";
export default function ServicePage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderItems />
<Service />
<NearestLocation />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,18 @@
import Footer from "@/components/landing-page/footer";
import HeaderPriceInformation from "@/components/landing-page/header-price";
import HeaderProduct from "@/components/landing-page/header-product-j7-awd";
import Navbar from "@/components/landing-page/navbar";
import HeaderProgramSales from "@/components/landing-page/program-sales";
import HeaderPromo from "@/components/landing-page/promo";
export default function ProgramSalesPage() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<HeaderProgramSales />
<Footer />
</div>
</div>
);
}

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,41 @@
// components/custom-editor.js
import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor";
function CustomEditor(props) {
return (
<CKEditor
editor={Editor}
data={props.initialData}
onChange={(event, editor) => {
const data = editor.getData();
console.log({ event, editor, data });
props.onChange(data);
}}
config={{
toolbar: [
"heading",
"fontsize",
"bold",
"italic",
"link",
"numberedList",
"bulletedList",
"undo",
"redo",
"alignment",
"outdent",
"indent",
"blockQuote",
"insertTable",
"codeBlock",
"sourceEditing",
],
}}
/>
);
}
export default CustomEditor;

View File

@ -0,0 +1,19 @@
import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor";
function ViewEditor(props) {
return (
<CKEditor
editor={Editor}
data={props.initialData}
disabled={true}
config={{
// toolbar: [],
isReadOnly: true,
}}
/>
);
}
export default ViewEditor;

View File

@ -0,0 +1,875 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import dynamic from "next/dynamic";
import { useDropzone } from "react-dropzone";
import { CloudUploadIcon, TimesIcon } from "@/components/icons";
import Image from "next/image";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { htmlToString } from "@/utils/global";
import { close, error, loading, successToast } from "@/config/swal";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Cookies from "js-cookie";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
createArticle,
createArticleSchedule,
getArticleByCategory,
uploadArticleFile,
uploadArticleThumbnail,
} from "@/service/article";
import {
saveManualContext,
updateManualArticle,
} from "@/service/generate-article";
import { getUserLevels } from "@/service/user-levels-service";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { getCategoryById } from "@/service/master-categories";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import GenerateSingleArticleForm from "./generate-ai-single-form";
import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
interface FileWithPreview extends File {
preview: string;
}
interface CategoryType {
id: number;
label: string;
value: number;
}
const categorySchema = z.object({
id: z.number(),
label: z.string(),
value: z.number(),
});
interface DiseData {
id: number;
articleBody: string;
title: string;
metaTitle: string;
description: string;
metaDescription: string;
mainKeyword: string;
additionalKeywords: string;
}
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
slug: z.string().min(2, {
message: "Slug harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
category: z.array(categorySchema).nonempty({
message: "Kategori harus memiliki setidaknya satu item",
}),
tags: z.array(z.string()).nonempty({
message: "Minimal 1 tag",
}),
});
export default function CreateArticleForm() {
const userLevel = Cookies.get("ulne");
const animatedComponents = makeAnimated();
const MySwal = withReactContent(Swal);
const router = useRouter();
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [useAi, setUseAI] = useState(false);
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [tag, setTag] = useState("");
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null
);
const [thumbnailValidation, setThumbnailValidation] = useState("");
const [filesValidation, setFileValidation] = useState("");
const [diseData, setDiseData] = useState<DiseData>();
const [selectedWritingType, setSelectedWritingType] = useState("single");
const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
"publish"
);
const [isScheduled, setIsScheduled] = useState(false);
const [startDateValue, setStartDateValue] = useState<any>(null);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles((prevFiles) => [
...prevFiles,
...acceptedFiles.map((file) => Object.assign(file)),
]);
},
multiple: true,
accept: {
"image/*": [],
},
});
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", category: [], tags: [] },
};
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
watch,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
fetchCategory();
}, []);
const fetchCategory = async () => {
const res = await getArticleByCategory();
if (res?.data?.data) {
setupCategory(res?.data?.data);
}
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
setListCategory(temp);
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
if ((thumbnailImg.length < 1 && !selectedMainImage) || files.length < 1) {
if (files.length < 1) {
setFileValidation("Required");
} else {
setFileValidation("");
}
if (thumbnailImg.length < 1 && !selectedMainImage) {
setThumbnailValidation("Required");
} else {
setThumbnailValidation("");
}
} else {
setThumbnailValidation("");
setFileValidation("");
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(values);
}
});
}
};
useEffect(() => {
if (useAi === false) {
setValue("description", "");
}
}, [useAi]);
function removeImgTags(htmlString: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(String(htmlString), "text/html");
const images = doc.querySelectorAll("img");
images.forEach((img) => img.remove());
return doc.body.innerHTML;
}
const saveArticleToDise = async (
values: z.infer<typeof createArticleSchema>
) => {
if (useAi) {
const request = {
id: diseData?.id,
title: values.title,
articleBody: removeImgTags(values.description),
metaDescription: diseData?.metaDescription,
metaTitle: diseData?.metaTitle,
mainKeyword: diseData?.mainKeyword,
additionalKeywords: diseData?.additionalKeywords,
createdBy: "345",
style: "Informational",
projectId: 2,
clientId: "humasClientIdtest",
lang: "id",
};
const res = await updateManualArticle(request);
if (res.error) {
error(res.message);
return false;
}
return diseData?.id;
} else {
const request = {
title: values.title,
articleBody: removeImgTags(values.description),
metaDescription: values.title,
metaTitle: values.title,
mainKeyword: values.title,
additionalKeywords: values.title,
createdBy: "345",
style: "Informational",
projectId: 2,
clientId: "humasClientIdtest",
lang: "id",
};
const res = await saveManualContext(request);
if (res.error) {
res.message;
return 0;
}
return res?.data?.data?.id;
}
};
const getUserLevelApprovalStatus = async () => {
const res = await getUserLevels(String(userLevel));
return res?.data?.data?.isApprovalActive;
};
const save = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const userLevelStatus = await getUserLevelApprovalStatus();
const formData = {
title: values.title,
typeId: 1,
slug: values.slug,
categoryIds: values.category.map((a) => a.id).join(","),
tags: values.tags.join(","),
description: htmlToString(removeImgTags(values.description)),
htmlDescription: removeImgTags(values.description),
aiArticleId: await saveArticleToDise(values),
// isDraft: userLevelStatus ? true : status === "draft",
// isPublish: userLevelStatus ? false : status === "publish",
isDraft: status === "draft",
isPublish: status === "publish",
};
const response = await createArticle(formData);
if (response?.error) {
error(response.message);
return false;
}
const articleId = response?.data?.data?.id;
if (files?.length > 0) {
const formFiles = new FormData();
for (const element of files) {
formFiles.append("file", element);
const resFile = await uploadArticleFile(articleId, formFiles);
}
}
if (thumbnailImg?.length > 0 || files?.length > 0) {
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadArticleThumbnail(articleId, formFiles);
} else {
const formFiles = new FormData();
if (selectedMainImage) {
formFiles.append("files", files[selectedMainImage - 1]);
const resFile = await uploadArticleThumbnail(articleId, formFiles);
}
}
}
if (status === "scheduled") {
const request = {
id: articleId,
date: `${startDateValue?.year}-${startDateValue?.month}-${startDateValue?.day}`,
};
const res = await createArticleSchedule(request);
}
close();
successSubmit("/admin/article", articleId, values.slug);
};
function successSubmit(redirect: string, id: number, slug: string) {
const url =
`${window.location.protocol}//${window.location.host}` +
"/news/detail/" +
`${id}-${slug}`;
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
successToast("Article Url", url);
} else {
router.push(redirect);
successToast("Article Url", url);
}
});
}
const watchTitle = watch("title");
const generateSlug = (title: string) => {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
};
useEffect(() => {
setValue("slug", generateSlug(watchTitle));
}, [watchTitle]);
const renderFilePreview = (file: FileWithPreview) => {
if (file.type.startsWith("image")) {
return (
<Image
width={48}
height={48}
alt={file.name}
src={URL.createObjectURL(file)}
className=" rounded border p-0.5"
/>
);
} else {
return "Not Found";
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const fileList = files.map((file, index) => (
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
<div className="file-preview">{renderFilePreview(file)}</div>
<div>
<div className=" text-sm text-card-foreground">{file.name}</div>
<div className=" text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
<div className="flex items-center space-x-2">
<Checkbox
id={String(index)}
value={String(index)}
checked={selectedMainImage === index + 1}
onCheckedChange={() => setSelectedMainImage(index + 1)}
/>
<label htmlFor={String(index)} className="text-black text-xs">
Jadikan Thumbnail
</label>
</div>
</div>
</div>
<Button
className="rounded-full"
variant="ghost"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button>
</div>
));
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files;
if (selectedFiles) {
setThumbnailImg(Array.from(selectedFiles));
}
};
const selectedCategory = watch("category");
useEffect(() => {
getDetailCategory();
}, [selectedCategory]);
const getDetailCategory = async () => {
let temp = getValues("tags");
for (const element of selectedCategory) {
const res = await getCategoryById(element?.id);
const tagList = res?.data?.data?.tags;
if (tagList) {
temp = [...temp, ...res?.data?.data?.tags];
}
}
const uniqueArray = temp.filter(
(item, index) => temp.indexOf(item) === index
);
setValue("tags", uniqueArray as [string, ...string[]]);
};
return (
<form
className="flex flex-col lg:flex-row gap-8 text-black"
onSubmit={handleSubmit(onSubmit)}
>
<div className="w-full lg:w-[65%] bg-white rounded-lg p-8 flex flex-col gap-1">
<p className="text-sm">Judul</p>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
id="title"
type="text"
placeholder="Masukkan judul artikel"
className="w-full border rounded-lg dark:border-gray-400"
{...field}
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm mb-3">{errors.title?.message}</p>
)}
<p className="text-sm mt-3">Slug</p>
<Controller
control={control}
name="slug"
render={({ field }) => (
<Input
type="text"
id="title"
placeholder=""
value={field.value ?? ""}
onChange={field.onChange}
className="w-full border rounded-lg dark:border-gray-400"
/>
)}
/>
{errors?.slug && (
<p className="text-red-400 text-sm mb-3">{errors.slug?.message}</p>
)}
<div className="flex items-center gap-2 mt-3">
<Switch checked={useAi} onCheckedChange={setUseAI} />
<p className="text-sm text-black">Bantuan AI</p>
</div>
{useAi && (
<div className="flex flex-col gap-2">
<Select
value={selectedWritingType ?? ""}
onValueChange={(value) => {
if (value !== "") setSelectedWritingType(value);
}}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
<SelectItem value="single">Single Article</SelectItem>
<SelectItem value="rewrite">Content Rewrite</SelectItem>
</SelectContent>
</Select>
{selectedWritingType === "single" ? (
<GenerateSingleArticleForm
content={(data) => {
setDiseData(data);
setValue(
"description",
data?.articleBody ? data?.articleBody : ""
);
}}
/>
) : (
<GenerateContentRewriteForm
content={(data) => {
setDiseData(data);
setValue(
"description",
data?.articleBody ? data?.articleBody : ""
);
}}
/>
)}
</div>
)}
<p className="text-sm mt-3">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor onChange={onChange} initialData={value} />
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm mb-3">
{errors.description?.message}
</p>
)}
<p className="text-sm mt-3">File Media</p>
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUploadIcon size={50} className="text-gray-300" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
maksimal 100mb.)
</div>
</div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className="flex justify-between gap-2">
<Button onClick={() => setFiles([])} size="sm">
Hapus Semua
</Button>
</div>
</Fragment>
) : null}
</Fragment>
{filesValidation !== "" && files.length < 1 && (
<p className="text-red-400 text-sm mb-3">Upload File Media</p>
)}
</div>
<div className="w-full lg:w-[35%] flex flex-col gap-8">
<div className="h-fit bg-white rounded-lg p-8 flex flex-col gap-1">
<p className="text-sm">Thubmnail</p>
{selectedMainImage && files.length >= selectedMainImage ? (
<div className="flex flex-row">
<img
src={URL.createObjectURL(files[selectedMainImage - 1])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
className="border-none rounded-full"
variant="outline"
size="sm"
onClick={() => setSelectedMainImage(null)}
>
<TimesIcon />
</Button>
</div>
) : thumbnailImg.length > 0 ? (
<div className="flex flex-row">
<img
src={URL.createObjectURL(thumbnailImg[0])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
className="border-none rounded-full"
variant="outline"
size="sm"
onClick={() => setThumbnailImg([])}
>
<TimesIcon />
</Button>
</div>
) : (
<>
{/* <label htmlFor="file-upload">
<button>Upload Thumbnail</button>
</label>{" "} */}
<input
id="file-upload"
type="file"
multiple
className="w-fit h-fit"
accept="image/*"
onChange={handleFileChange}
/>
{thumbnailValidation !== "" && (
<p className="text-red-400 text-sm mb-3">
Upload thumbnail atau pilih dari File Media
</p>
)}
</>
)}
<p className="text-sm mt-3">Kategori</p>
<Controller
control={control}
name="category"
render={({ field: { onChange, value } }) => (
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={true}
placeholder="Kategori..."
name="sub-module"
options={listCategory}
/>
)}
/>
{errors?.category && (
<p className="text-red-400 text-sm mb-3">
{errors.category?.message}
</p>
)}
<p className="text-sm">Tags</p>
{/* <Controller
control={control}
name="tags"
render={({ field: { onChange, value } }) => (
<Textarea
type="text"
id="tags"
placeholder=""
label=""
value={tag}
onValueChange={setTag}
startContent={
<div className="flex flex-wrap gap-1">
{value.map((item, index) => (
<Chip
color="primary"
key={index}
className=""
onClose={() => {
const filteredTags = value.filter((tag) => tag !== item);
if (filteredTags.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue("tags", filteredTags as [string, ...string[]]);
}
}}
>
{item}
</Chip>
))}
</div>
}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
labelPlacement="outside"
className="w-full h-fit"
classNames={{
inputWrapper: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
variant="bordered"
/>
)}
/> */}
<Controller
control={control}
name="tags"
render={({ field: { value } }) => (
<div className="w-full">
{/* Menampilkan tags */}
<div className="flex flex-wrap gap-1 mb-2">
{value.map((item: string, index: number) => (
<Badge
key={index}
className="flex items-center gap-1 px-2 py-1 text-sm"
variant="secondary"
>
{item}
<button
type="button"
onClick={() => {
const filteredTags = value.filter(
(tag: string) => tag !== item
);
if (filteredTags.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue(
"tags",
filteredTags as [string, ...string[]]
);
}
}}
className="text-red-500 text-xs ml-1"
>
×
</button>
</Badge>
))}
</div>
{/* Textarea input */}
<Textarea
id="tags"
placeholder="Tekan Enter untuk menambahkan tag"
value={tag ?? ""}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
clearErrors("tags");
}
}
}}
className="border rounded-lg"
aria-label="Tags Input"
/>
</div>
)}
/>
{errors?.tags && (
<p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p>
)}
<div className="flex flex-col gap-2 mt-3">
<div className="flex items-center space-x-2">
<Switch
id="schedule-switch"
checked={isScheduled}
onCheckedChange={setIsScheduled}
/>
<label htmlFor="schedule-switch" className="text-black text-sm">
Publish dengan Jadwal
</label>
</div>
{/* {isScheduled && (
<div className="flex flex-col lg:flex-row gap-3">
<div className="w-full lg:w-[140px] flex flex-col gal-2 ">
<p className="text-sm">Tanggal</p>
<Popover>
<PopoverTrigger>
<Button
type="button"
className="w-full !h-[30px] lg:h-[40px] border-1 rounded-lg text-black"
variant="outline"
>
{startDateValue
? convertDateFormatNoTime(startDateValue)
: "-"}
</Button>
</PopoverTrigger>
<PopoverContent className="bg-transparent p-0">
<Calendar
selected={startDateValue}
onSelect={setStartDateValue}
/>
</PopoverContent>
</Popover>
</div>
</div>
)} */}
</div>
</div>
<div className="flex flex-row justify-end gap-3">
<Button
color="primary"
type="submit"
disabled={isScheduled && startDateValue == null}
onClick={() =>
isScheduled ? setStatus("scheduled") : setStatus("publish")
}
>
Publish
</Button>
<Button
color="success"
type="submit"
onClick={() => setStatus("draft")}
>
<p className="text-white">Draft</p>
</Button>
<Link href="/admin/article">
<Button variant="outline" type="button">
Kembali
</Button>
</Link>
</div>
</div>
</form>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,342 @@
"use client";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { useEffect, useState } from "react";
import { close, error, loading } from "@/config/swal";
import { delay } from "@/utils/global";
import dynamic from "next/dynamic";
import GetSeoScore from "./get-seo-score-form";
import {
getDetailArticle,
getGenerateRewriter,
} from "@/service/generate-article";
import { Button } from "@/components/ui/button";
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
const writingStyle = [
{
id: 1,
name: "Friendly",
},
{
id: 1,
name: "Professional",
},
{
id: 3,
name: "Informational",
},
{
id: 4,
name: "Neutral",
},
{
id: 5,
name: "Witty",
},
];
const articleSize = [
{
id: 1,
name: "News (300 - 900 words)",
value: "News",
},
{
id: 2,
name: "Info (900 - 2000 words)",
value: "Info",
},
{
id: 3,
name: "Detail (2000 - 5000 words)",
value: "Detail",
},
];
interface DiseData {
id: number;
articleBody: string;
title: string;
metaTitle: string;
description: string;
metaDescription: string;
mainKeyword: string;
additionalKeywords: string;
}
export default function GenerateContentRewriteForm(props: {
content: (data: DiseData) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState("");
const [articleIds, setArticleIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number>();
const [isLoading, setIsLoading] = useState(true);
const onSubmit = async () => {
loading();
const request = {
advConfig: "",
context: mainKeyword,
style: selectedWritingSyle,
sentiment: "Informational",
urlContext: null,
contextType: "article",
lang: selectedLanguage,
createdBy: "123123",
clientId: "humasClientIdtest",
};
const res = await getGenerateRewriter(request);
close();
if (res?.error) {
error("Error");
}
setArticleIds([...articleIds, res?.data?.data?.id]);
};
useEffect(() => {
getArticleDetail();
}, [selectedId]);
const checkArticleStatus = async (data: string | null) => {
if (data === null) {
delay(7000).then(() => {
getArticleDetail();
});
}
};
const getArticleDetail = async () => {
if (selectedId) {
const res = await getDetailArticle(selectedId);
const data = res?.data?.data;
checkArticleStatus(data?.articleBody);
if (data?.articleBody !== null) {
setIsLoading(false);
props.content(data);
} else {
setIsLoading(true);
props.content({
id: data?.id,
articleBody: "",
title: "",
metaTitle: "",
description: "",
metaDescription: "",
additionalKeywords: "",
mainKeyword: "",
});
}
}
};
return (
<fieldset>
<form className="flex flex-col w-full mt-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
{/* <Select
label="Writing Style"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedWritingSyle]}
onChange={(e) =>
e.target.value !== ""
? setSelectedWritingStyle(e.target.value)
: ""
}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
>
<SelectSection>
{writingStyle.map((style) => (
<SelectItem key={style.name}>{style.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<div className="w-full space-y-1">
<label className="text-sm font-medium text-black dark:text-white">
Writing Style
</label>
<Select
value={selectedWritingSyle}
onValueChange={(value) => setSelectedWritingStyle(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{writingStyle.map((style) => (
<SelectItem key={style.name} value={style.name}>
{style.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* <Select
label="Article Size"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedArticleSize]}
onChange={(e) => (e.target.value !== "" ? setSelectedArticleSize(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
{articleSize.map((size) => (
<SelectItem key={size.value}>{size.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<div className="w-full space-y-1">
<label className="text-sm font-medium text-black dark:text-white">
Article Size
</label>
<Select
value={selectedArticleSize}
onValueChange={(value) => setSelectedArticleSize(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{articleSize.map((size) => (
<SelectItem key={size.name} value={size.name}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* <Select
label="Bahasa"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedLanguage]}
onChange={(e) => (e.target.value !== "" ? setSelectedLanguage(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
<SelectItem key="id">Indonesia</SelectItem>
<SelectItem key="en">English</SelectItem>
</SelectSection>
</Select> */}
<div className="w-full space-y-1">
<label className="text-sm font-medium text-black dark:text-white">
Language
</label>
<Select
value={selectedLanguage}
onValueChange={(value) => setSelectedLanguage(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col mt-3">
<div className="flex flex-row gap-2 items-center">
<p className="text-sm">Text</p>
</div>
<div className="w-[78vw] lg:w-full">
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
</div>
{mainKeyword == "" && (
<p className="text-red-400 text-sm">Required</p>
)}
{/* {articleIds.length < 3 && (
<Button color="primary" className="my-5 w-full py-5 text-xs md:text-base" type="button" onPress={onSubmit} isDisabled={mainKeyword == ""}>
Generate
</Button>
)} */}
{articleIds.length < 3 && (
<Button
onClick={onSubmit}
type="button"
disabled={mainKeyword === ""}
className="my-5 w-full py-5 text-xs md:text-base"
>
Generate
</Button>
)}
</div>
{articleIds.length > 0 && (
<div className="flex flex-row gap-1 mt-2">
{/* {articleIds?.map((id, index) => (
<Button onPress={() => setSelectedId(id)} key={id} isLoading={isLoading && selectedId == id} color={selectedId == id && isLoading ? "warning" : selectedId == id ? "success" : "default"}>
<p className={selectedId == id ? "text-white" : "text-black"}>Article {index + 1}</p>
</Button>
))} */}
{articleIds?.map((id, index) => (
<Button
key={id}
onClick={() => setSelectedId(id)}
disabled={isLoading && selectedId === id}
variant={
selectedId === id
? isLoading
? "secondary"
: "default"
: "outline"
}
className={
selectedId === id ? "bg-green-600 text-white" : "text-black"
}
>
{isLoading && selectedId === id
? "Loading..."
: `Article ${index + 1}`}
</Button>
))}
</div>
)}
{!isLoading && (
<div>
<GetSeoScore id={String(selectedId)} />
</div>
)}
</form>
</fieldset>
);
}

View File

@ -0,0 +1,287 @@
"use client";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { useEffect, useState } from "react";
import { close, error, loading } from "@/config/swal";
import { delay } from "@/utils/global";
import dynamic from "next/dynamic";
import { getDetailArticle, getGenerateRewriter } from "@/service/generate-article";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import GetSeoScore from "./get-seo-score-form";
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
const writingStyle = [
{
id: 1,
name: "Friendly",
},
{
id: 1,
name: "Professional",
},
{
id: 3,
name: "Informational",
},
{
id: 4,
name: "Neutral",
},
{
id: 5,
name: "Witty",
},
];
const articleSize = [
{
id: 1,
name: "News (300 - 900 words)",
value: "News",
},
{
id: 2,
name: "Info (900 - 2000 words)",
value: "Info",
},
{
id: 3,
name: "Detail (2000 - 5000 words)",
value: "Detail",
},
];
interface DiseData {
id: number;
articleBody: string;
title: string;
metaTitle: string;
description: string;
metaDescription: string;
mainKeyword: string;
additionalKeywords: string;
}
export default function GenerateContentRewriteForm(props: { content: (data: DiseData) => void }) {
const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational");
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState("");
const [articleIds, setArticleIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number>();
const [isLoading, setIsLoading] = useState(true);
const onSubmit = async () => {
loading();
const request = {
advConfig: "",
context: mainKeyword,
style: selectedWritingSyle,
sentiment: "Informational",
urlContext: null,
contextType: "article",
lang: selectedLanguage,
createdBy: "123123",
clientId: "humasClientIdtest",
};
const res = await getGenerateRewriter(request);
close();
if (res?.error) {
error("Error");
}
setArticleIds([...articleIds, res?.data?.data?.id]);
};
useEffect(() => {
getArticleDetail();
}, [selectedId]);
const checkArticleStatus = async (data: string | null) => {
if (data === null) {
delay(7000).then(() => {
getArticleDetail();
});
}
};
const getArticleDetail = async () => {
if (selectedId) {
const res = await getDetailArticle(selectedId);
const data = res?.data?.data;
checkArticleStatus(data?.articleBody);
if (data?.articleBody !== null) {
setIsLoading(false);
props.content(data);
} else {
setIsLoading(true);
props.content({
id: data?.id,
articleBody: "",
title: "",
metaTitle: "",
description: "",
metaDescription: "",
additionalKeywords: "",
mainKeyword: "",
});
}
}
};
return (
<fieldset>
<form className="flex flex-col w-full mt-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
{/* <Select
label="Writing Style"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedWritingSyle]}
onChange={(e) =>
e.target.value !== ""
? setSelectedWritingStyle(e.target.value)
: ""
}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
>
<SelectSection>
{writingStyle.map((style) => (
<SelectItem key={style.name}>{style.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<Select value={selectedWritingSyle} onValueChange={(value) => setSelectedWritingStyle(value)}>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{writingStyle.map((style) => (
<SelectItem key={style.name} value={style.name}>
{style.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
label="Article Size"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedArticleSize]}
onChange={(e) => (e.target.value !== "" ? setSelectedArticleSize(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
{articleSize.map((size) => (
<SelectItem key={size.value}>{size.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<Select value={selectedArticleSize} onValueChange={(value) => setSelectedArticleSize(value)}>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{articleSize.map((style) => (
<SelectItem key={style.name} value={style.name}>
{style.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
label="Bahasa"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedLanguage]}
onChange={(e) => (e.target.value !== "" ? setSelectedLanguage(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
<SelectItem key="id">Indonesia</SelectItem>
<SelectItem key="en">English</SelectItem>
</SelectSection>
</Select> */}
<Select value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)}>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col mt-3">
<div className="flex flex-row gap-2 items-center">
<p className="text-sm">Text</p>
</div>
<div className="w-[78vw] lg:w-full">
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
</div>
{mainKeyword == "" && <p className="text-red-400 text-sm">Required</p>}
{articleIds.length < 3 && (
<Button onClick={onSubmit} type="button" disabled={mainKeyword === "" || isLoading} className="my-5 w-full py-5 text-xs md:text-base">
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
"Generate"
)}
</Button>
)}
</div>
{articleIds.length > 0 && (
<div className="flex flex-row gap-1 mt-2">
{articleIds?.map((id, index) => (
<Button key={id} onClick={() => setSelectedId(id)} disabled={isLoading && selectedId === id} variant={selectedId === id ? "default" : "outline"} className="flex items-center gap-2">
{isLoading && selectedId === id ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</>
) : (
`Article ${index + 1}`
)}
</Button>
))}
</div>
)}
{!isLoading && (
<div>
<GetSeoScore id={String(selectedId)} />
</div>
)}
</form>
</fieldset>
);
}

View File

@ -0,0 +1,448 @@
"use client";
import { useEffect, useState } from "react";
import { close, error, loading } from "@/config/swal";
import { delay } from "@/utils/global";
import GetSeoScore from "./get-seo-score-form";
import {
generateDataArticle,
getDetailArticle,
getGenerateKeywords,
getGenerateTitle,
} from "@/service/generate-article";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { Input } from "@/components/ui/input";
const writingStyle = [
{
id: 1,
name: "Friendly",
},
{
id: 1,
name: "Professional",
},
{
id: 3,
name: "Informational",
},
{
id: 4,
name: "Neutral",
},
{
id: 5,
name: "Witty",
},
];
const articleSize = [
{
id: 1,
name: "News (300 - 900 words)",
value: "News",
},
{
id: 2,
name: "Info (900 - 2000 words)",
value: "Info",
},
{
id: 3,
name: "Detail (2000 - 5000 words)",
value: "Detail",
},
];
interface DiseData {
id: number;
articleBody: string;
title: string;
metaTitle: string;
description: string;
metaDescription: string;
mainKeyword: string;
additionalKeywords: string;
}
export default function GenerateSingleArticleForm(props: {
content: (data: DiseData) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState("");
const [title, setTitle] = useState("");
const [additionalKeyword, setAdditionalKeyword] = useState("");
const [articleIds, setArticleIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number>();
const [isLoading, setIsLoading] = useState(true);
const generateAll = async (keyword: string | undefined) => {
if (keyword) {
generateTitle(keyword);
generateKeywords(keyword);
}
};
const generateTitle = async (keyword: string | undefined) => {
if (keyword) {
loading();
const req = {
keyword: keyword,
style: selectedWritingSyle,
website: "None",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
clientId: "",
};
const res = await getGenerateTitle(req);
const data = res?.data?.data;
setTitle(data);
close();
}
};
const generateKeywords = async (keyword: string | undefined) => {
if (keyword) {
const req = {
keyword: keyword,
style: selectedWritingSyle,
website: "None",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "0",
clientId: "",
};
loading();
const res = await getGenerateKeywords(req);
const data = res?.data?.data;
setAdditionalKeyword(data);
close();
}
};
const onSubmit = async () => {
loading();
const request = {
advConfig: "",
style: selectedWritingSyle,
website: "None",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
title: title,
imageSource: "Web",
mainKeyword: mainKeyword,
additionalKeywords: additionalKeyword,
targetCountry: null,
articleSize: selectedArticleSize,
projectId: 2,
createdBy: "123123",
clientId: "humasClientIdtest",
};
const res = await generateDataArticle(request);
close();
if (res?.error) {
error("Error");
}
setArticleIds([...articleIds, res?.data?.data?.id]);
// props.articleId(res?.data?.data?.id);
};
useEffect(() => {
getArticleDetail();
}, [selectedId]);
const checkArticleStatus = async (data: string | null) => {
if (data === null) {
delay(7000).then(() => {
getArticleDetail();
});
}
};
const getArticleDetail = async () => {
if (selectedId) {
const res = await getDetailArticle(selectedId);
const data = res?.data?.data;
checkArticleStatus(data?.articleBody);
if (data?.articleBody !== null) {
setIsLoading(false);
props.content(data);
} else {
setIsLoading(true);
props.content({
id: data?.id,
articleBody: "",
title: "",
metaTitle: "",
description: "",
metaDescription: "",
additionalKeywords: "",
mainKeyword: "",
});
}
}
};
return (
<fieldset>
<form className="flex flex-col w-full mt-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
{/* <Select
label="Writing Style"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedWritingSyle]}
onChange={(e) =>
e.target.value !== ""
? setSelectedWritingStyle(e.target.value)
: ""
}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
>
<SelectSection>
{writingStyle.map((style) => (
<SelectItem key={style.name}>{style.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<Select
value={selectedWritingSyle}
onValueChange={(value) => {
if (value !== "") setSelectedWritingStyle(value);
}}
>
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{writingStyle.map((style) => (
<SelectItem key={style.name} value={style.name}>
{style.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
label="Article Size"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedArticleSize]}
onChange={(e) => (e.target.value !== "" ? setSelectedArticleSize(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
{articleSize.map((size) => (
<SelectItem key={size.value}>{size.name}</SelectItem>
))}
</SelectSection>
</Select> */}
<Select
value={selectedArticleSize}
onValueChange={(value) => {
if (value !== "") setSelectedArticleSize(value);
}}
>
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
<SelectValue placeholder="Writing Style" />
</SelectTrigger>
<SelectContent>
{articleSize.map((style) => (
<SelectItem key={style.name} value={style.name}>
{style.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
label="Bahasa"
variant="bordered"
labelPlacement="outside"
placeholder=""
selectedKeys={[selectedLanguage]}
onChange={(e) => (e.target.value !== "" ? setSelectedLanguage(e.target.value) : "")}
className="w-full"
classNames={{
label: "!text-black",
value: "!text-black",
trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
>
<SelectSection>
<SelectItem key="id">Indonesia</SelectItem>
<SelectItem key="en">English</SelectItem>
</SelectSection>
</Select> */}
<Select
value={selectedLanguage}
onValueChange={(value) => {
if (value !== "") setSelectedLanguage(value);
}}
>
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
<SelectValue placeholder="Bahasa" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col mt-3">
<div className="flex flex-row gap-2 items-center">
<p className="text-sm">Main Keyword</p>
<Button
variant="default"
size="sm"
onClick={() => generateAll(mainKeyword)}
disabled={isLoading} // tambahkan state kontrol loading
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Processing...
</>
) : (
"Process"
)}
</Button>
</div>
<Input
type="text"
id="mainKeyword"
placeholder="Masukkan keyword utama"
value={mainKeyword}
onChange={(e) => setMainKeyword(e.target.value)}
className="w-full mt-1 border border-gray-300 rounded-lg dark:border-gray-400"
/>
{mainKeyword == "" && (
<p className="text-red-400 text-sm">Required</p>
)}
<div className="flex flex-row gap-2 items-center mt-3">
<p className="text-sm">Title</p>
<Button
variant="default"
size="sm"
onClick={() => generateTitle(mainKeyword)}
disabled={mainKeyword === ""}
>
Generate
</Button>
</div>
<Input
type="text"
id="title"
placeholder=""
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full mt-1 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" // Custom styling using className
aria-label="Title"
/>
{/* {title == "" && <p className="text-red-400 text-sm">Required</p>} */}
<div className="flex flex-row gap-2 items-center mt-2">
<p className="text-sm">Additional Keyword</p>
<Button
className="text-sm"
size="sm"
onClick={() => generateKeywords(mainKeyword)}
disabled={mainKeyword === ""}
>
Generate
</Button>
</div>
<Input
type="text"
id="additionalKeyword"
placeholder=""
value={additionalKeyword}
onChange={(e) => setAdditionalKeyword(e.target.value)}
className="mt-1 border rounded-lg dark:bg-transparent dark:border-gray-400"
aria-label="Additional Keyword"
/>
{/* {additionalKeyword == "" && (
<p className="text-red-400 text-sm">Required</p>
)} */}
{/* {articleIds.length < 3 && (
<Button color="primary" className="my-5 w-full py-5 text-xs md:text-base" type="button" onPress={onSubmit} isDisabled={mainKeyword == "" || title == "" || additionalKeyword == ""}>
Generate
</Button>
)} */}
{articleIds.length < 3 && (
<Button
className="my-5 w-full py-5 text-xs md:text-base"
type="button"
onClick={onSubmit}
disabled={
mainKeyword === "" || title === "" || additionalKeyword === ""
}
>
Generate
</Button>
)}
</div>
{articleIds.length > 0 && (
<div className="flex flex-row gap-1 mt-2">
{articleIds.map((id, index) => (
<Button
key={id}
onClick={() => setSelectedId(id)}
disabled={isLoading && selectedId === id}
className={`
${
selectedId === id
? isLoading
? "bg-yellow-500"
: "bg-green-600"
: "bg-gray-200"
}
text-sm px-4 py-2 rounded text-white transition-colors
`}
>
Article {index + 1}
</Button>
))}
</div>
)}
{!isLoading && (
<div>
<GetSeoScore id={String(selectedId)} />
</div>
)}
</form>
</fieldset>
);
}

View File

@ -0,0 +1,197 @@
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { CustomCircularProgress } from "@/components/layout/costum-circular-progress";
import { getSeoScore } from "@/service/generate-article";
import { useEffect, useState } from "react";
export default function GetSeoScore(props: { id: string }) {
useEffect(() => {
fetchSeoScore();
}, [props.id]);
const [totalScoreSEO, setTotalScoreSEO] = useState();
const [errorSEO, setErrorSEO] = useState<any>([]);
const [warningSEO, setWarningSEO] = useState<any>([]);
const [optimizedSEO, setOptimizedSEO] = useState<any>([]);
const fetchSeoScore = async () => {
const res = await getSeoScore(props?.id);
if (res.error) {
// error(res.message);
return false;
}
setTotalScoreSEO(res.data.data?.seo_analysis?.score || 0);
const errorList: any[] = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.error,
...res.data.data?.seo_analysis?.analysis?.content_quality?.error,
];
setErrorSEO(errorList);
const warningList: any[] = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.warning,
...res.data.data?.seo_analysis?.analysis?.content_quality?.warning,
];
setWarningSEO(warningList);
const optimizedList: any[] = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.optimized,
...res.data.data?.seo_analysis?.analysis?.content_quality?.optimized,
];
setOptimizedSEO(optimizedList);
};
return (
<div className="overflow-y-auto my-2">
<div className="text-black flex flex-col rounded-md gap-3">
<p className="font-semibold text-lg"> SEO Score</p>
{totalScoreSEO ? (
<div className="flex flex-row gap-5 w-full">
{/* <CircularProgress
aria-label=""
color="warning"
showValueLabel={true}
size="lg"
value={Number(totalScoreSEO) * 100}
/> */}
<CustomCircularProgress value={Number(totalScoreSEO) * 100} />
<div>
{/* <ApexChartDonut value={Number(totalScoreSEO) * 100} /> */}
</div>
<div className="flex flex-row gap-5">
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-red-500 rounded-lg">
{/* <TimesIcon size={15} className="text-danger" /> */}
Error : {errorSEO.length || 0}
</div>
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-yellow-500 rounded-lg">
{/* <p className="text-warning w-[15px] h-[15px] text-center mt-[-10px]">
!
</p> */}
Warning : {warningSEO.length || 0}
</div>
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-green-500 rounded-lg">
{/* <CheckIcon size={15} className="text-success" /> */}
Optimize : {optimizedSEO.length || 0}
</div>
</div>
</div>
) : (
"Belum ada Data"
)}
{totalScoreSEO && (
// <Accordion
// variant="splitted"
// itemClasses={{
// base: "!bg-transparent",
// title: "text-black",
// }}
// >
// <AccordionItem
// key="1"
// aria-label="Error"
// // startContent={<TimesIcon size={20} className="text-danger" />}
// title={`${errorSEO?.length || 0} Errors`}
// >
// <div className="flex flex-col gap-2">
// {errorSEO?.map((item: any) => (
// <p key={item} className="w-full border border-red-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
// {item}
// </p>
// ))}
// </div>
// </AccordionItem>
// <AccordionItem
// key="2"
// aria-label="Warning"
// // startContent={
// // <p className="text-warning w-[20px] h-[20px] text-center mt-[-10px]">
// // !
// // </p>
// // }
// title={`${warningSEO?.length || 0} Warnings`}
// >
// <div className="flex flex-col gap-2">
// {warningSEO?.map((item: any) => (
// <p key={item} className="w-full border border-yellow-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
// {item}
// </p>
// ))}
// </div>
// </AccordionItem>
// <AccordionItem
// key="3"
// aria-label="Optimized"
// // startContent={<CheckIcon size={20} className="text-success" />}
// title={`${optimizedSEO?.length || 0} Optimized`}
// >
// <div className="flex flex-col gap-2">
// {optimizedSEO?.map((item: any) => (
// <p key={item} className="w-full border border-green-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
// {item}
// </p>
// ))}
// </div>
// </AccordionItem>
// </Accordion>
<Accordion type="multiple" className="w-full">
<AccordionItem value="error">
<AccordionTrigger className="text-black">{`${
errorSEO?.length || 0
} Errors`}</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-2">
{errorSEO?.map((item: any) => (
<p
key={item}
className="w-full border border-red-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
>
{item}
</p>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="warning">
<AccordionTrigger className="text-black">{`${
warningSEO?.length || 0
} Warnings`}</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-2">
{warningSEO?.map((item: any) => (
<p
key={item}
className="w-full border border-yellow-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
>
{item}
</p>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="optimized">
<AccordionTrigger className="text-black">{`${
optimizedSEO?.length || 0
} Optimized`}</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-2">
{optimizedSEO?.map((item: any) => (
<p
key={item}
className="w-full border border-green-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
>
{item}
</p>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,442 @@
"use client";
import { close, error, loading } from "@/config/swal";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { z } from "zod";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { editMasterUsers, getDetailMasterUsers } from "@/service/master-user";
import { getAllUserLevels } from "@/service/user-levels-service";
import { listUserRole } from "@/service/master-user-role";
import { Card } from "../ui/card";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "../ui/button";
const userSchema = z.object({
id: z.number(),
label: z.string(),
value: z.string(),
});
const masterUserSchema = z.object({
fullname: z.string().min(1, { message: "Required" }),
username: z.string().min(1, { message: "Required" }),
email: z.string().min(1, { message: "Required" }),
identityNumber: z.string().min(1, { message: "Required" }),
genderType: z.string().min(1, { message: "Required" }),
phoneNumber: z.string().min(1, { message: "Required" }),
address: z.string().min(1, { message: "Required" }),
userLevelType: userSchema,
userRoleType: userSchema,
});
export default function FormMasterUserEdit() {
const router = useRouter();
const MySwal = withReactContent(Swal);
const animatedComponents = makeAnimated();
const params = useParams();
const id = params?.id;
const [parentList, setParentList] = useState<any>([]);
const [listRole, setListRole] = useState<any>([]);
const formOptions = {
resolver: zodResolver(masterUserSchema),
};
type MicroIssueSchema = z.infer<typeof masterUserSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
} = useForm<MicroIssueSchema>(formOptions);
async function save(data: z.infer<typeof masterUserSchema>) {
const formData = {
address: data.address,
email: data.email,
fullname: data.fullname,
genderType: data.genderType,
identityNumber: data.identityNumber,
phoneNumber: data.phoneNumber,
userLevelId: data.userLevelType.id,
userRoleId: data.userRoleType.id,
username: data.username,
};
const response = await editMasterUsers(formData, String(id));
if (response?.error) {
error(response.message);
return false;
}
successSubmit("/admin/master-user");
}
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
async function onSubmit(data: z.infer<typeof masterUserSchema>) {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
useEffect(() => {
initFetch();
}, [id]);
const initFetch = async () => {
loading();
const res = await getDetailMasterUsers(String(id));
const profile = res?.data?.data;
const listLevel = await fetchUserLevel();
const listRole = await fetchUserRole();
const findLevel = listLevel?.find((a) => a.id === profile.userLevelId);
const findRole = listRole?.find((a) => a.id === profile.userRoleId);
setValue("fullname", profile?.fullname);
setValue("username", profile?.username);
setValue("email", profile?.email);
setValue("address", profile?.address);
setValue("identityNumber", profile?.identityNumber);
setValue("genderType", profile?.genderType);
setValue("phoneNumber", profile?.phoneNumber);
if (findLevel) {
setValue("userLevelType", findLevel);
}
if (findRole) {
setValue("userRoleType", findRole);
}
close();
};
const fetchUserLevel = async () => {
const res = await getAllUserLevels();
if (res?.data?.data) {
return setupParent(res?.data?.data, "level");
}
};
const fetchUserRole = async () => {
const request = {
limit: 100,
page: 1,
};
const res = await listUserRole(request);
if (res?.data?.data) {
return setupParent(res?.data?.data, "role");
}
};
const setupParent = (data: any, type: "level" | "role") => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.name,
value: element.aliasName || element.code,
});
}
if (type === "level") {
setParentList(temp);
} else {
setListRole(temp);
}
return temp;
};
return (
<div className="mx-5 my-5 overflow-y-auto">
<form
method="POST"
onSubmit={handleSubmit(onSubmit)}
className="w-full lg:w-1/2 lg:ml-4"
>
<Card className="rounded-md p-5 flex flex-col gap-3">
<Controller
control={control}
name="fullname"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="title">Nama Lengkap</Label>
<Input
type="text"
id="title"
placeholder="Nama Lengkap..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.fullname?.message && (
<p className="text-red-400 text-sm">{errors.fullname?.message}</p>
)}
<Controller
control={control}
name="username"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="username">Username</Label>
<Input
type="text"
id="username"
placeholder="Username..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.username?.message && (
<p className="text-red-400 text-sm">{errors.username?.message}</p>
)}
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="email">Email</Label>
<Input
type="email"
id="email"
placeholder="Email..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.email?.message && (
<p className="text-red-400 text-sm">{errors.email?.message}</p>
)}
{/* <Controller
control={control}
name="identityType"
render={({ field: { onChange, value } }) => (
<Select
variant="bordered"
labelPlacement="outside"
label="Identity Type"
placeholder="Select"
className="max-w-xs"
selectedKeys={[value]}
onChange={onChange}
>
{typeIdentity.map((type) => (
<SelectItem key={type.value}>{type.value}</SelectItem>
))}
</Select>
)}
/>
{errors.identityType?.message && (
<p className="text-red-400 text-sm">
{errors.identityType?.message}
</p>
)} */}
<Controller
control={control}
name="identityNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="identityNumber">NRP</Label>
<Input
type="number"
id="identityNumber"
placeholder="NRP..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.identityNumber?.message && (
<p className="text-red-400 text-sm">
{errors.identityNumber?.message}
</p>
)}
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="alamat">Alamat</Label>
<Textarea
id="alamat"
placeholder="Alamat..."
value={value ?? ""}
onChange={(e) => onChange(e)}
className="border border-gray-300 dark:border-gray-400 rounded-lg dark:bg-transparent"
/>
</div>
)}
/>
{errors.address?.message && (
<p className="text-red-400 text-sm">{errors.address?.message}</p>
)}
<Controller
control={control}
name="genderType"
render={({ field: { onChange, value } }) => (
<div className="space-y-2">
<Label htmlFor="gender">Gender</Label>
<RadioGroup
id="gender"
value={value}
onValueChange={onChange}
className="flex flex-row gap-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Male" id="male" />
<Label htmlFor="male">Laki-laki</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Female" id="female" />
<Label htmlFor="female">Perempuan</Label>
</div>
</RadioGroup>
</div>
)}
/>
{errors.genderType?.message && (
<p className="text-red-400 text-sm">{errors.genderType?.message}</p>
)}
<Controller
control={control}
name="userLevelType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Level Pengguna</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={value}
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={parentList}
/>
</>
)}
/>
{errors.userLevelType?.message && (
<p className="text-red-400 text-sm">
{errors.userLevelType?.message}
</p>
)}
<Controller
control={control}
name="userRoleType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Peran Pengguna</p>
<ReactSelect
className="basic-single text-black z-49"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
value={value}
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={listRole}
/>
</>
)}
/>
{errors.userRoleType?.message && (
<p className="text-red-400 text-sm">
{errors.userRoleType?.message}
</p>
)}
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full z-0 space-y-2">
<Label htmlFor="identityNumber">No. Handphone</Label>
<Input
type="number"
id="identityNumber"
placeholder="08*********"
value={value ?? ""}
onChange={onChange}
/>
</div>
)}
/>
{errors.phoneNumber?.message && (
<p className="text-red-400 text-sm">
{errors.phoneNumber?.message}
</p>
)}
<div className="flex justify-end gap-3">
<Link href={`/admin/master-user`}>
<Button color="danger" variant="ghost">
Cancel
</Button>
</Link>
<Button type="submit" color="primary" variant="default">
Save
</Button>
</div>
</Card>
</form>
</div>
);
}

View File

@ -0,0 +1,577 @@
"use client";
import { close, error, loading } from "@/config/swal";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { z } from "zod";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import ReactPasswordChecklist from "react-password-checklist";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { zodResolver } from "@hookform/resolvers/zod";
import { createMasterUser } from "@/service/master-user";
import { getAllUserLevels } from "@/service/user-levels-service";
import { listUserRole } from "@/service/master-user-role";
import { Card } from "../ui/card";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "../ui/button";
const userSchema = z.object({
id: z.number(),
label: z.string(),
value: z.string(),
});
const masterUserSchema = z.object({
fullname: z.string().min(1, { message: "Required" }),
username: z.string().min(1, { message: "Required" }),
password: z
.string()
.min(8, "Password harus memiliki minimal 8 karakter.")
.refine((password) => /[A-Z]/.test(password), {
message: "Password harus memiliki minimal satu huruf kapital.",
})
.refine((password) => /[0-9]/.test(password), {
message: "Password harus memiliki minimal satu angka.",
})
.refine((password) => /[!@#$%^&*(),.?":{}|<>]/.test(password), {
message: "Password harus memiliki minimal satu simbol.",
}),
passwordValidate: z.string().min(1, { message: "Required" }),
email: z.string().min(1, { message: "Required" }),
identityNumber: z.string().min(1, { message: "Required" }),
genderType: z.string().min(1, { message: "Required" }),
phoneNumber: z.string().min(1, { message: "Required" }),
address: z.string().min(1, { message: "Required" }),
userLevelType: userSchema,
userRoleType: userSchema,
});
export default function FormMasterUser() {
const router = useRouter();
const animatedComponents = makeAnimated();
const MySwal = withReactContent(Swal);
const [isVisible, setIsVisible] = useState([false, false]);
const [isValidPassword, setIsValidPassword] = useState(false);
const [parentList, setParentList] = useState<any>([]);
const [listRole, setListRole] = useState<any>([]);
const toggleVisibility = (type: number) => {
setIsVisible(
type === 0 ? [!isVisible[0], isVisible[1]] : [isVisible[0], !isVisible[1]]
);
};
const formOptions = {
resolver: zodResolver(masterUserSchema),
defaultValues: { password: "", passwordValidate: "" },
};
type MicroIssueSchema = z.infer<typeof masterUserSchema>;
const {
control,
handleSubmit,
formState: { errors },
setError,
watch,
setValue,
} = useForm<MicroIssueSchema>(formOptions);
const passwordVal = watch("password");
const passwordConfVal = watch("passwordValidate");
async function save(data: z.infer<typeof masterUserSchema>) {
const formData = {
address: data.address,
password: data.password,
email: data.email,
fullname: data.fullname,
genderType: data.genderType,
identityNumber: data.identityNumber,
identityType: "nrp",
phoneNumber: data.phoneNumber,
userLevelId: data.userLevelType.id,
userRoleId: data.userRoleType.id,
username: data.username,
};
const response = await createMasterUser(formData);
if (response?.error) {
error(response.message);
return false;
}
successSubmit("/admin/master-user");
}
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
async function onSubmit(data: z.infer<typeof masterUserSchema>) {
if (data.password === data.passwordValidate) {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
} else {
setError("passwordValidate", {
type: "manual",
message: "Password harus sama.",
});
}
}
const generatePassword = () => {
const length = Math.floor(Math.random() * 9) + 8;
const upperCaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lowerCaseChars = "abcdefghijklmnopqrstuvwxyz";
const numberChars = "0123456789";
const specialChars = "!@#$%^&*";
const allChars =
upperCaseChars + lowerCaseChars + numberChars + specialChars;
let generatedPassword = "";
generatedPassword +=
upperCaseChars[Math.floor(Math.random() * upperCaseChars.length)];
generatedPassword +=
specialChars[Math.floor(Math.random() * specialChars.length)];
generatedPassword +=
numberChars[Math.floor(Math.random() * numberChars.length)];
generatedPassword +=
lowerCaseChars[Math.floor(Math.random() * lowerCaseChars.length)];
for (let i = generatedPassword.length; i < length; i++) {
generatedPassword +=
allChars[Math.floor(Math.random() * allChars.length)];
}
generatedPassword = generatedPassword
.split("")
.sort(() => 0.5 - Math.random())
.join("");
setValue("password", generatedPassword);
};
useEffect(() => {
fetchUserLevel();
fetchUserRole();
}, []);
const fetchUserLevel = async () => {
loading();
const res = await getAllUserLevels();
close();
if (res?.data?.data) {
setupParent(res?.data?.data, "level");
}
};
const fetchUserRole = async () => {
loading();
const request = {
limit: 100,
page: 1,
};
const res = await listUserRole(request);
close();
if (res?.data?.data) {
setupParent(res?.data?.data, "role");
}
};
const setupParent = (data: any, type: "level" | "role") => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.name,
value: element.aliasName || element.code,
});
}
if (type === "level") {
setParentList(temp);
} else {
setListRole(temp);
}
};
return (
<div className="mx-5 my-5 overflow-y-auto">
<form method="POST" onSubmit={handleSubmit(onSubmit)}>
<Card className="rounded-md p-5 flex flex-col gap-3">
<Controller
control={control}
name="fullname"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="title"
className="mb-1 block text-sm font-medium"
>
Nama Lengkap
</Label>
<Input
type="text"
id="title"
placeholder="Nama Lengkap..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.fullname?.message && (
<p className="text-red-400 text-sm">{errors.fullname?.message}</p>
)}
<Controller
control={control}
name="username"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="username"
className="mb-1 block text-sm font-medium"
>
Username
</Label>
<Input
type="text"
id="username"
placeholder="Username..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.username?.message && (
<p className="text-red-400 text-sm">{errors.username?.message}</p>
)}
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="email"
className="mb-1 block text-sm font-medium"
>
Email
</Label>
<Input
type="email"
id="email"
placeholder="Email..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.email?.message && (
<p className="text-red-400 text-sm">{errors.email?.message}</p>
)}
<Controller
control={control}
name="identityNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="identityNumber"
className="mb-1 block text-sm font-medium"
>
NRP
</Label>
<Input
type="number"
id="identityNumber"
placeholder="NRP..."
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.identityNumber?.message && (
<p className="text-red-400 text-sm">
{errors.identityNumber?.message}
</p>
)}
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label
htmlFor="alamat"
className="mb-1 block text-sm font-medium"
>
Alamat
</Label>
<Textarea
id="alamat"
placeholder="Alamat..."
value={value ?? ""}
onChange={(e) => onChange(e.target.value)}
className="border border-gray-300 dark:border-gray-400 rounded-lg bg-white dark:bg-transparent text-black dark:text-white"
/>
</div>
)}
/>
{errors.address?.message && (
<p className="text-red-400 text-sm">{errors.address?.message}</p>
)}
<Controller
control={control}
name="genderType"
render={({ field: { onChange, value } }) => (
<div className="w-full">
<Label className="mb-2 block text-sm font-medium">Gender</Label>
<RadioGroup
className="flex flex-row gap-4"
value={value}
onValueChange={onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Male" id="male" />
<Label htmlFor="male">Laki-laki</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Female" id="female" />
<Label htmlFor="female">Perempuan</Label>
</div>
</RadioGroup>
</div>
)}
/>
{errors.genderType?.message && (
<p className="text-red-400 text-sm">{errors.genderType?.message}</p>
)}
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="identityNumber">No. Handphone</Label>
<Input
type="number"
id="identityNumber"
placeholder="08*********"
value={value ?? ""}
onChange={onChange}
className="w-full"
/>
</div>
)}
/>
{errors.phoneNumber?.message && (
<p className="text-red-400 text-sm">
{errors.phoneNumber?.message}
</p>
)}
<Controller
control={control}
name="userLevelType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Level Pengguna</p>
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={parentList}
/>
</>
)}
/>
{errors.userLevelType?.message && (
<p className="text-red-400 text-sm">
{errors.userLevelType?.message}
</p>
)}
<Controller
control={control}
name="userRoleType"
render={({ field: { onChange, value } }) => (
<>
<p className="text-sm mt-3">Peran Pengguna</p>
<ReactSelect
className="basic-single text-black z-49"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}}
classNamePrefix="select"
onChange={onChange}
closeMenuOnSelect={false}
components={animatedComponents}
isClearable={true}
isSearchable={true}
isMulti={false}
placeholder=""
name="sub-module"
options={listRole}
/>
</>
)}
/>
{errors.userRoleType?.message && (
<p className="text-red-400 text-sm">
{errors.userRoleType?.message}
</p>
)}
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2 relative z-0">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
type={isVisible[0] ? "text" : "password"}
id="password"
placeholder="Password..."
value={value ?? ""}
onChange={onChange}
className="w-full pr-10"
/>
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-2xl text-muted-foreground focus:outline-none"
type="button"
onClick={() => toggleVisibility(0)}
>
{isVisible[0] ? (
<EyeSlashFilledIcon className="pointer-events-none" />
) : (
<EyeFilledIcon className="pointer-events-none" />
)}
</button>
</div>
</div>
)}
/>
{errors.password?.message && (
<p className="text-red-400 text-sm">{errors.password?.message}</p>
)}
<Controller
control={control}
name="passwordValidate"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2 relative z-0">
<Label htmlFor="passwordValidate">Konfirmasi Password</Label>
<div className="relative">
<Input
type={isVisible[1] ? "text" : "password"}
id="passwordValidate"
placeholder="Konfirmasi Password..."
value={value ?? ""}
onChange={onChange}
className="w-full pr-10"
/>
<button
className="absolute right-2 top-1/2 -translate-y-1/2 text-2xl text-muted-foreground focus:outline-none"
type="button"
onClick={() => toggleVisibility(1)}
>
{isVisible[1] ? (
<EyeSlashFilledIcon className="pointer-events-none" />
) : (
<EyeFilledIcon className="pointer-events-none" />
)}
</button>
</div>
</div>
)}
/>
{errors.passwordValidate?.message && (
<p className="text-red-400 text-sm">
{errors.passwordValidate?.message}
</p>
)}
<a
className="cursor-pointer text-[#DD8306]"
onClick={generatePassword}
>
Generate Password
</a>
<ReactPasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={passwordVal}
valueAgain={passwordConfVal}
onChange={(isValid) => {
setIsValidPassword(isValid);
}}
className="text-black dark:text-white text-sm my-3"
messages={{
minLength: "Password must be more than 8 characters",
specialChar: "Password must include a special character",
number: "Password must include a number",
capital: "Password must include an uppercase letter",
match: "Passwords match",
}}
/>
<div className="flex justify-end gap-3">
<Link href={`/admin/master-user`}>
<Button color="danger" variant="ghost">
Cancel
</Button>
</Link>
<Button type="submit" color="primary" variant="default">
Save
</Button>
</div>
</Card>
</form>
</div>
);
}

526
components/form/login.tsx Normal file
View File

@ -0,0 +1,526 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import Cookies from "js-cookie";
import { close, error, loading } from "@/config/swal";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import {
checkUsernames,
emailValidation,
getProfile,
postSignIn,
setupEmail,
} from "@/service/master-user";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { saveActivity } from "@/service/activity-log";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import Swal from "sweetalert2";
export default function Login() {
const router = useRouter();
const [isVisible, setIsVisible] = useState(false);
const [isVisibleSetup, setIsVisibleSetup] = useState([false, false]);
const [oldEmail, setOldEmail] = useState("");
const [newEmail, setNewEmail] = useState("");
const [passwordSetup, setPasswordSetup] = useState("");
const [confPasswordSetup, setConfPasswordSetup] = useState("");
const toggleVisibility = () => setIsVisible(!isVisible);
const [needOtp, setNeedOtp] = useState(false);
const [isFirstLogin, setFirstLogin] = useState(false);
const [otpValue, setOtpValue] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [accessData, setAccessData] = useState<any>();
const [profile, setProfile] = useState<any>();
const [isValidEmail, setIsValidEmail] = useState(false);
const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal);
const setValUsername = (e: any) => {
const uname = e.replaceAll(/[^\w.-]/g, "");
setUsername(uname.toLowerCase());
};
const onSubmit = async () => {
const data = {
username: username,
password: password,
};
if (!username || !password) {
error("Username & Password Wajib Diisi !");
} else {
loading();
const response = await postSignIn(data);
if (response?.error) {
error("Username / Password Tidak Sesuai");
} else {
const profile = await getProfile(response?.data?.data?.access_token);
const dateTime: any = new Date();
const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
Cookies.set("access_token", response?.data?.data?.access_token, {
expires: 1,
});
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
expires: 1,
});
Cookies.set("time_refresh", newTime, {
expires: 1,
});
Cookies.set("is_first_login", "true", {
secure: true,
sameSite: "strict",
});
await saveActivity(
{
activityTypeId: 1,
url: "https://dev.mikulnews.com/auth",
userId: profile?.data?.data?.id,
},
response?.data?.data?.access_token
);
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
});
Cookies.set("uie", profile?.data?.data?.id, {
expires: 1,
});
Cookies.set("ufne", profile?.data?.data?.fullname, {
expires: 1,
});
Cookies.set("ulie", profile?.data?.data?.userLevelGroup, {
expires: 1,
});
Cookies.set("username", profile?.data?.data?.username, {
expires: 1,
});
Cookies.set("urie", profile?.data?.data?.roleId, {
expires: 1,
});
Cookies.set("roleName", profile?.data?.data?.roleName, {
expires: 1,
});
Cookies.set("masterPoldaId", profile?.data?.data?.masterPoldaId, {
expires: 1,
});
Cookies.set("ulne", profile?.data?.data?.userLevelId, {
expires: 1,
});
Cookies.set("urce", profile?.data?.data?.roleCode, {
expires: 1,
});
Cookies.set("email", profile?.data?.data?.email, {
expires: 1,
});
router.push("/admin/dashboard");
Cookies.set("status", "login", {
expires: 1,
});
close();
}
}
};
const checkUsername = async () => {
const res = await checkUsernames(checkUsernameValue);
if (res?.error) {
error("Username tidak ditemukan");
return false;
}
MySwal.fire({
title: "",
text: "",
html: (
<>
<p>
Kami telah mengirimkan tautan untuk mengatur ulang kata sandi ke
email Anda
</p>
<p className="text-xs">
Apakah Anda sudah menerima emailnya? Jika belum, periksa folder spam
Anda
</p>
</>
),
icon: "info",
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const submitCheckEmail = async () => {
const req = {
oldEmail: oldEmail,
newEmail: newEmail,
username: username,
password: password,
};
const res = await setupEmail(req);
if (res?.error) {
if (res.message?.messages[0]) {
error(res.message?.messages[0]);
} else {
error(res?.message);
}
return false;
}
close();
setNeedOtp(true);
setFirstLogin(false);
};
return (
<div className="min-h-screen flex">
{/* Left Side - Logo Section */}
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-gray-600 via-gray-700 to-gray-800 relative overflow-hidden">
<div className="absolute inset-0 bg-black/20"></div>
<div className="relative z-10 flex items-center justify-center w-full p-12">
<div className="text-center">
<Link href={"/"}>
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-8 shadow-2xl border border-white/20">
<img
src="/masjaecoo.png"
alt="Mikul News Logo"
className="max-w-xs h-auto drop-shadow-lg"
/>
</div>
</Link>
<div className="mt-8 text-white/90">
<h2 className="text-2xl font-bold mb-2">Portal Jaecoo</h2>
<p className="text-sm opacity-80">Platform beyond classic</p>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-20 w-32 h-32 bg-white/5 rounded-full blur-2xl"></div>
</div>
{/* Right Side - Login Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-8 bg-gray-50">
<div className="w-full max-w-md">
{/* Mobile Logo */}
<div className="lg:hidden text-center mb-8">
<Link href={"/"}>
<img
src="/masjaecoonav.png"
alt="Mikul News Logo"
className="w-64 h-10 mx-auto"
/>
</Link>
</div>
{isFirstLogin ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-orange-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Setup Akun
</h2>
<p className="text-gray-600">Lengkapi informasi email Anda</p>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="old-email"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Email Lama
</Label>
<Input
id="old-email"
type="email"
required
placeholder="Masukkan email lama"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
value={oldEmail}
onChange={(e) => setOldEmail(e.target.value)}
/>
</div>
<div>
<Label
htmlFor="new-email"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Email Baru
</Label>
<Input
id="new-email"
type="email"
required
placeholder="Masukkan email baru"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
/>
</div>
<Button
size="lg"
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={submitCheckEmail}
>
Submit
</Button>
</div>
</div>
) : needOtp ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Verifikasi OTP
</h2>
<p className="text-gray-600">
Masukkan kode OTP yang telah dikirim
</p>
</div>
</div>
) : isResetPassword ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Reset Password
</h2>
<p className="text-gray-600">
Masukkan username untuk reset password
</p>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="reset-username"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Username
</Label>
<Input
id="reset-username"
type="text"
required
placeholder="Masukkan username"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors"
value={checkUsernameValue}
onChange={(e) =>
setCheckUsernameValue(e.target.value.trim())
}
onPaste={(e) =>
setCheckUsernameValue(e.currentTarget.value.trim())
}
onCopy={(e) =>
setCheckUsernameValue(e.currentTarget.value.trim())
}
/>
</div>
<Button
size="lg"
className="w-full bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
onClick={checkUsername}
disabled={checkUsernameValue === ""}
>
Check Username
</Button>
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
<Link
href={`/`}
className="text-sm text-gray-600 hover:text-gray-900 transition-colors lg:hidden"
>
Beranda
</Link>
<button
className="text-sm text-red-600 hover:text-red-700 font-medium transition-colors"
onClick={() => setIsResetPassword(false)}
>
Kembali ke Login
</button>
</div>
</div>
</div>
) : (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Selamat Datang
</h2>
<p className="text-gray-600">
Portal Jaecoo - Platform beyond classic
</p>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="username"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Username
</Label>
<Input
id="username"
required
type="text"
placeholder="Masukkan username"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
value={username}
onChange={(e) => setValUsername(e.target.value.trim())}
onPaste={(e) =>
setValUsername(e.currentTarget.value.trim())
}
onCopy={(e) => setValUsername(e.currentTarget.value.trim())}
/>
</div>
<div>
<Label
htmlFor="password"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Password
</Label>
<div className="relative">
<Input
id="password"
required
type={isVisible ? "text" : "password"}
placeholder="Masukkan password"
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="button"
onClick={toggleVisibility}
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
>
{isVisible ? (
<EyeSlashFilledIcon className="w-5 h-5" />
) : (
<EyeFilledIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
<Button
size="lg"
className="w-full bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={onSubmit}
>
Masuk ke Portal
</Button>
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
<Link
href={`/`}
className="text-sm text-gray-600 hover:text-gray-900 transition-colors lg:hidden"
>
Beranda
</Link>
<button
className="text-sm text-red-600 hover:text-red-700 font-medium transition-colors"
onClick={() => setIsResetPassword(true)}
>
Lupa Password?
</button>
</div>
</div>
{/* <div className="mt-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start">
<svg className="w-5 h-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">Informasi Portal</p>
<p className="text-blue-700">Akses informasi terkini dan status permintaan informasi yang telah diajukan.</p>
</div>
</div>
</div> */}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,179 @@
"use client";
import { useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import PasswordChecklist from "react-password-checklist";
import { close, error, loading } from "@/config/swal";
import { savePassword } from "@/service/master-user";
import { Input } from "@/components/ui/input";
import { Eye, EyeOff } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function PasswordForm(props: { doFetch: () => void }) {
const MySwal = withReactContent(Swal);
const [isVisible, setIsVisible] = useState([false, false]);
const [passwordConf, setPasswordConf] = useState("");
const [password, setPassword] = useState("");
const [isValidPassword, setIsValidPassword] = useState(false);
const onSubmit = async () => {
loading();
const data = {
password: password,
confirmPassword: passwordConf,
};
const res = await savePassword(data);
if (res?.error) {
error(res.message);
return false;
}
close();
props.doFetch();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const generatePassword = () => {
const length = Math.floor(Math.random() * 9) + 8;
const upperCaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lowerCaseChars = "abcdefghijklmnopqrstuvwxyz";
const numberChars = "0123456789";
const specialChars = "!@#$%^&*";
const allChars =
upperCaseChars + lowerCaseChars + numberChars + specialChars;
let generatedPassword = "";
generatedPassword +=
upperCaseChars[Math.floor(Math.random() * upperCaseChars.length)];
generatedPassword +=
specialChars[Math.floor(Math.random() * specialChars.length)];
generatedPassword +=
numberChars[Math.floor(Math.random() * numberChars.length)];
generatedPassword +=
lowerCaseChars[Math.floor(Math.random() * lowerCaseChars.length)];
for (let i = generatedPassword.length; i < length; i++) {
generatedPassword +=
allChars[Math.floor(Math.random() * allChars.length)];
}
generatedPassword = generatedPassword
.split("")
.sort(() => 0.5 - Math.random())
.join("");
setPassword(generatedPassword);
};
return (
<form className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<p className="text-sm">Password</p>
<div className="relative">
<Input
required
type={isVisible[0] ? "text" : "password"}
placeholder=""
className="w-full pr-10 border border-gray-300 rounded-lg dark:border-gray-400"
value={password}
onChange={(e) => {
const target = e.target as HTMLInputElement;
setPassword(target.value.trim());
}}
onPaste={(e) => {
const target = e.target as HTMLInputElement;
setPassword(target.value.trim());
}}
onCopy={(e) => {
const target = e.target as HTMLInputElement;
setPassword(target.value.trim());
}}
/>
<button
type="button"
onClick={() => setIsVisible([!isVisible[0], isVisible[1]])}
className="absolute inset-y-0 right-2 flex items-center text-gray-400 focus:outline-none"
>
{isVisible[0] ? (
<EyeOff className="w-5 h-5 pointer-events-none" />
) : (
<Eye className="w-5 h-5 pointer-events-none" />
)}
</button>
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Konfirmasi Password</p>
<div className="relative">
<Input
required
type={isVisible[1] ? "text" : "password"}
placeholder=""
className="w-full pr-10 border border-gray-300 rounded-lg dark:border-gray-400"
value={passwordConf}
onChange={(e) => {
const target = e.target as HTMLInputElement;
setPasswordConf(target.value.trim());
}}
onPaste={(e) => {
const target = e.target as HTMLInputElement;
setPasswordConf(target.value.trim());
}}
onCopy={(e) => {
const target = e.target as HTMLInputElement;
setPasswordConf(target.value.trim());
}}
/>
<button
type="button"
onClick={() => setIsVisible([isVisible[0], !isVisible[1]])}
className="absolute inset-y-0 right-2 flex items-center text-gray-400 focus:outline-none"
>
{isVisible[1] ? (
<EyeOff className="w-5 h-5 pointer-events-none" />
) : (
<Eye className="w-5 h-5 pointer-events-none" />
)}
</button>
</div>
</div>
<a className="cursor-pointer text-[#DD8306]" onClick={generatePassword}>
Generate Password
</a>
<PasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={password}
valueAgain={passwordConf}
onChange={(isValid) => {
setIsValidPassword(isValid);
}}
className="text-black dark:text-white text-sm my-3"
messages={{
minLength: "Password must be more than 8 characters",
specialChar: "Password must include a special character",
number: "Password must include a number",
capital: "Password must include an uppercase letter",
match: "Passwords match",
}}
/>
<Button
className="w-fit mt-4 bg-blue-600 text-white hover:bg-blue-700"
onClick={onSubmit}
disabled={!isValidPassword}
>
Simpan
</Button>
</form>
);
}

View File

@ -0,0 +1,290 @@
"use client";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { updateProfile } from "@/service/master-user";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "@/components/ui/button";
const formSchema = z.object({
fullname: z.string().min(1, {
message: "Harus diisi",
}),
username: z.string().min(1, {
message: "Harus diisi",
}),
email: z
.string()
.email({
message: "Email tidak valid",
})
.min(2, {
message: "Harus diisi",
}),
nrp: z.string().min(1, {
message: "Harus diisi",
}),
address: z.string().min(1, {
message: "Harus diisi",
}),
gender: z.string().min(1, {
message: "Harus diisi",
}),
phoneNumber: z.string().min(1, {
message: "Harus diisi",
}),
});
export default function ProfileForm(props: {
profile: any;
doFetch: () => void;
}) {
const MySwal = withReactContent(Swal);
const { profile } = props;
const formOptions = {
resolver: zodResolver(formSchema),
};
type UserSettingSchema = z.infer<typeof formSchema>;
const {
control,
handleSubmit,
formState: { errors },
setValue,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
setValue("fullname", profile?.fullname);
setValue("username", profile?.username);
setValue("email", profile?.email);
setValue("address", profile?.address);
setValue("nrp", profile?.identityNumber);
setValue("gender", profile?.genderType);
setValue("phoneNumber", profile?.phoneNumber);
}, [profile]);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
loading();
const req = {
address: values.address,
fullname: values.fullname,
username: values.username,
email: values.email,
identityNumber: values.nrp,
phoneNumber: values.phoneNumber,
genderType: values.gender,
userLevelId: profile.userLevelId,
userRoleId: profile.userRoleId,
};
const res = await updateProfile(req);
close();
if (res?.error) {
error(res.message);
return false;
}
props.doFetch();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
return (
<form className="flex flex-col gap-3 " onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-1">
<p className="text-sm">Username</p>
<Controller
control={control}
name="username"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="username"
placeholder=""
readOnly
value={value ?? ""}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400"
/>
)}
/>
{errors?.username && (
<p className="text-red-400 text-sm mb-3">
{errors.username?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="fullname"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<label htmlFor="fullname" className="text-sm">
Nama Lengkap
</label>
<Input
type="text"
id="fullname"
placeholder=""
value={value}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400 dark:bg-transparent"
/>
</div>
)}
/>
{errors?.fullname && (
<p className="text-red-400 text-sm mb-3">
{errors.fullname?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<label htmlFor="email" className="text-sm">
Email
</label>
<Input
type="email"
id="email"
placeholder=""
value={value}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400 dark:bg-transparent"
/>
</div>
)}
/>
{errors?.email && (
<p className="text-red-400 text-sm mb-3">{errors.email?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="nrp"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<label htmlFor="nrp" className="text-sm">
NRP
</label>
<Input
type="number"
id="nrp"
placeholder=""
value={value}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400 dark:bg-transparent"
/>
</div>
)}
/>
{errors?.nrp && (
<p className="text-red-400 text-sm mb-3">{errors.nrp?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<label htmlFor="address" className="text-sm">
Alamat
</label>
<Textarea
id="address"
placeholder=""
value={value}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400 dark:bg-transparent"
/>
</div>
)}
/>
{errors?.address && (
<p className="text-red-400 text-sm mb-3">{errors.address?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Gender</p>
<Controller
control={control}
name="gender"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<RadioGroup
value={value}
onValueChange={onChange}
className="flex flex-row gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="male" id="male" />
<label htmlFor="male" className="text-sm">
Laki-laki
</label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="female" id="female" />
<label htmlFor="female" className="text-sm">
Perempuan
</label>
</div>
</RadioGroup>
</div>
)}
/>
{errors?.gender && (
<p className="text-red-400 text-sm mb-3">{errors.gender?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<div className="flex flex-col gap-1">
<label htmlFor="phoneNumber" className="text-sm">
Nomor Handphone
</label>
<Input
type="number"
id="phoneNumber"
placeholder=""
value={value}
onChange={onChange}
className="w-full border border-gray-300 rounded-lg dark:border-gray-400 dark:bg-transparent"
/>
</div>
)}
/>
{errors?.phoneNumber && (
<p className="text-red-400 text-sm mb-3">
{errors.phoneNumber?.message}
</p>
)}
</div>
<Button color="primary" type="submit" className="w-fit mt-4">
Simpan
</Button>
</form>
);
}

View File

@ -0,0 +1,204 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import DOMPurify from "dompurify";
import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { close, error, loading } from "@/config/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import dynamic from "next/dynamic";
import { editCustomStaticPage, getCustomStaticDetail } from "@/service/static-page-service";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
const formSchema = z.object({
slug: z.string().min(2, {
message: "Slug must be at least 2 characters.",
}),
title: z.string().min(2, {
message: "Title must be at least 2 characters.",
}),
description: z.string().min(2, {
message: "Main Keyword must be at least 2 characters.",
}),
htmlBody: z.string().min(2, {
message: "Main Keyword must be at least 2 characters.",
}),
});
export default function StaticPageBuilderEdit() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const params = useParams();
const id = params.id;
const formOptions = {
resolver: zodResolver(formSchema),
};
type UserSettingSchema = z.infer<typeof formSchema>;
const {
control,
handleSubmit,
formState: { errors },
watch,
setValue,
getValues,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initFetch();
}, [id]);
const initFetch = async () => {
const res = await getCustomStaticDetail(id ? String(id) : "");
const data = res?.data?.data;
console.log("res", data);
setValue("title", data?.title);
setValue("slug", data?.slug);
setValue("description", data?.description);
setValue("htmlBody", addPreCode(data?.htmlBody));
};
const addPreCode = (htmlString: string): string => {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, "text/html");
const bodyContent = doc.body.innerHTML.trim();
return `<pre><code class="language-html">${bodyContent.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>`;
};
const content = watch("htmlBody");
const generatedPage = useCallback(() => {
const sanitizedContent = DOMPurify.sanitize(content);
const textArea = document.createElement("textarea");
textArea.innerHTML = sanitizedContent;
return (
<Card className="rounded-md border p-4">
<div dangerouslySetInnerHTML={{ __html: textArea.value }} />
</Card>
);
}, [content]);
function createSlug(value: string): string {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const request = {
id: Number(id),
title: values.title,
slug: values.slug,
description: values.description,
htmlBody: values.htmlBody,
};
loading();
const res = await editCustomStaticPage(request);
if (res?.error) {
error(res.message);
return false;
}
close();
successSubmit("/admin/static-page");
};
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
// const title = watch("title");
// useEffect(() => {
// if (getValues("title")) {
// setValue("slug", createSlug(getValues("title")));
// }
// }, [title]);
return (
<form className="flex flex-col gap-3 px-4" onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="title" className="text-sm font-medium">
Title
</Label>
<Input type="text" id="title" placeholder="Title" value={value ?? ""} onChange={onChange} />
</div>
)}
/>
{errors.title?.message && <p className="text-red-400 text-sm">{errors.title?.message}</p>}
<Controller
control={control}
name="slug"
render={({ field: { onChange, value } }) => (
<div className="w-full space-y-2">
<Label htmlFor="slug" className="text-sm font-medium">
Slug
</Label>
<Input type="text" id="slug" placeholder="Slug" value={value ?? ""} onChange={onChange} />
</div>
)}
/>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => <div className="space-y-2">
<Label htmlFor="description" className="text-sm font-medium">
Description
</Label>
<Textarea
id="description"
placeholder="Description"
value={value ?? ""}
onChange={onChange}
/>
</div>}
/>
{errors.description?.message && <p className="text-red-400 text-sm">{errors.description?.message}</p>}
<div className="grid grid-cols-1 md:grid-cols-2">
<div className="flex flex-col gap-2">
Editor
<Controller control={control} name="htmlBody" render={({ field: { onChange, value } }) => <CustomEditor onChange={onChange} initialData={value} />} />
</div>
<div className="px-4 flex flex-col gap-2">
Preview
{generatedPage()}
</div>
</div>
<div className="flex justify-end w-full">
<Button type="submit" color="primary" className="w-fit">
Save
</Button>
</div>
</form>
);
}

2738
components/icons.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,214 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const DashboardUserIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 3c2.21 0 4 1.79 4 4s-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4m4 10.54c0 1.06-.28 3.53-2.19 6.29L13 15l.94-1.88c-.62-.07-1.27-.12-1.94-.12s-1.32.05-1.94.12L11 15l-.81 4.83C8.28 17.07 8 14.6 8 13.54c-2.39.7-4 1.96-4 3.46v4h16v-4c0-1.5-1.6-2.76-4-3.46"
/>
</svg>
);
export const DashboardBriefcaseIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<path fill="#f5bc00" d="M44,41H4V10h40V41z" />
<polygon fill="#eb7900" points="44,26 24,26 4,26 4,10 44,10" />
<path fill="#eb7900" d="M17,26h-6v3h6V26z" />
<path fill="#eb7900" d="M37,26h-6v3h6V26z" />
<rect width="14" height="3" x="17" y="7" fill="#f5bc00" />
<path fill="#eb0000" d="M17,23h-6v3h6V23z" />
<path fill="#eb0000" d="M37,23h-6v3h6V23z" />
</svg>
);
export const DashboardMailboxIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<path fill="#3dd9eb" d="M43,36H13V11h22c4.418,0,8,3.582,8,8V36z" />
<path
fill="#7debf5"
d="M21,36H5V19c0-4.418,3.582-8,8-8l0,0c4.418,0,8,3.582,8,8V36z"
/>
<path fill="#6c19ff" d="M21,36h5v8h-5V36z" />
<polygon fill="#eb0000" points="27,16 27,20 35,20 35,24 39,24 39,16" />
<rect width="8" height="3" x="9" y="20" fill="#3dd9eb" />
</svg>
);
export const DashboardShareIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132c13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328"
/>
</svg>
);
export const DashboardSpeecIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M7 0a2 2 0 0 0-2 2h9a2 2 0 0 1 2 2v12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"
/>
<path
fill="currentColor"
d="M13 20a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2zM9 5h4v5H9zM4 5h4v1H4zm0 2h4v1H4zm0 2h4v1H4zm0 2h9v1H4zm0 2h9v1H4zm0 2h9v1H4z"
/>
</svg>
);
export const DashboardConnectIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 22V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18H6zm7.075-7.75L12 12.475l2.925 1.775l-.775-3.325l2.6-2.25l-3.425-.275L12 5.25L10.675 8.4l-3.425.275l2.6 2.25z"
/>
</svg>
);
export const DashboardTopLeftPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M18 18L6 6m0 0h9M6 6v9"
/>
</svg>
);
export const DashboardRightDownPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M5.47 5.47a.75.75 0 0 1 1.06 0l10.72 10.72V9a.75.75 0 0 1 1.5 0v9a.75.75 0 0 1-.75.75H9a.75.75 0 0 1 0-1.5h7.19L5.47 6.53a.75.75 0 0 1 0-1.06"
clipRule="evenodd"
/>
</svg>
);
export const DashboardCommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<defs>
<mask id="ipSComment0">
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path fill="#fff" stroke="#fff" d="M44 6H4v30h9v5l10-5h21z" />
<path stroke="#000" d="M14 19.5v3m10-3v3m10-3v3" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSComment0)" />
</svg>
);

View File

@ -0,0 +1,196 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const PdfIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 15 15"
{...props}
>
<path
fill="currentColor"
d="M3.5 8H3V7h.5a.5.5 0 0 1 0 1M7 10V7h.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM3.5 6H2v5h1V9h.5a1.5 1.5 0 1 0 0-3m4 0H6v5h1.5A1.5 1.5 0 0 0 9 9.5v-2A1.5 1.5 0 0 0 7.5 6m2.5 5V6h3v1h-2v1h1v1h-1v2z"
clipRule="evenodd"
/>
</svg>
);
export const CsvIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h3v1H3v3h2v1H2zm7 0H6v3h2v1H6v1h3V8H7V7h2zm2 0h-1v3.707l1.5 1.5l1.5-1.5V6h-1v3.293l-.5.5l-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const ExcelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
width={size || width}
height={size || height}
{...props}
>
<path
fill="currentColor"
d="M3.793 7.5L2.146 5.854l.708-.708L4.5 6.793l1.646-1.647l.708.708L5.207 7.5l1.647 1.646l-.708.708L4.5 8.207L2.854 9.854l-.708-.708z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const WordIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m2.015 5.621l1 4a.5.5 0 0 0 .901.156l.584-.876l.584.876a.5.5 0 0 0 .901-.156l1-4l-.97-.242l-.726 2.903l-.373-.56a.5.5 0 0 0-.832 0l-.373.56l-.726-2.903z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const PptIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="M3 8h.5a.5.5 0 0 0 0-1H3zm4 0h.5a.5.5 0 0 0 0-1H7z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h1.5a1.5 1.5 0 1 1 0 3H3v2H2zm4 0h1.5a1.5 1.5 0 1 1 0 3H7v2H6zm5 5h1V7h1V6h-3v1h1z"
clipRule="evenodd"
/>
</svg>
);
export const FileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m10.5.5l.354-.354L10.707 0H10.5zm3 3h.5v-.207l-.146-.147zm-1 10.5h-10v1h10zM2 13.5v-12H1v12zM2.5 1h8V0h-8zM13 3.5v10h1v-10zM10.146.854l3 3l.708-.708l-3-3zM2.5 14a.5.5 0 0 1-.5-.5H1A1.5 1.5 0 0 0 2.5 15zm10 1a1.5 1.5 0 0 0 1.5-1.5h-1a.5.5 0 0 1-.5.5zM2 1.5a.5.5 0 0 1 .5-.5V0A1.5 1.5 0 0 0 1 1.5z"
/>
</svg>
);
export const UserProfileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M11 7c0 1.66-1.34 3-3 3S5 8.66 5 7s1.34-3 3-3s3 1.34 3 3"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M16 8c0 4.42-3.58 8-8 8s-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8M4 13.75C4.16 13.484 5.71 11 7.99 11c2.27 0 3.83 2.49 3.99 2.75A6.98 6.98 0 0 0 14.99 8c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 2.38 1.19 4.49 3.01 5.75"
clipRule="evenodd"
/>
</svg>
);
export const SettingsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zm2.8-6.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5"
/>
</svg>
);

View File

@ -0,0 +1,487 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const MenuBurgerIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M3 6h18M3 12h18M3 18h18"
/>
</svg>
);
export const DashboardIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z"
/>
</svg>
);
export const HomeIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M2 12.204c0-2.289 0-3.433.52-4.381c.518-.949 1.467-1.537 3.364-2.715l2-1.241C9.889 2.622 10.892 2 12 2c1.108 0 2.11.622 4.116 1.867l2 1.241c1.897 1.178 2.846 1.766 3.365 2.715c.519.948.519 2.092.519 4.38v1.522c0 3.9 0 5.851-1.172 7.063C19.657 22 17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.212C2 19.576 2 17.626 2 13.725z" />
<path strokeLinecap="round" d="M12 15v3" />
</g>
</svg>
);
export const Submenu1Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<defs>
<mask id="ipTData0">
<g
fill="none"
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11" />
<path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20" />
<ellipse cx="24" cy="10" fill="#555" rx="20" ry="6" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTData0)" />
</svg>
);
export const Submenu2Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M230.93 220a8 8 0 0 1-6.93 4H32a8 8 0 0 1-6.92-12c15.23-26.33 38.7-45.21 66.09-54.16a72 72 0 1 1 73.66 0c27.39 8.95 50.86 27.83 66.09 54.16a8 8 0 0 1 .01 8"
/>
</svg>
);
export const InfoCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 1024 1024"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448s448-200.6 448-448S759.4 64 512 64m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372s372 166.6 372 372s-166.6 372-372 372"
/>
<path
fill="currentColor"
d="M464 336a48 48 0 1 0 96 0a48 48 0 1 0-96 0m72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8"
/>
</svg>
);
export const MinusCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 16 16"
width={size || width}
{...props}
>
<g fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8" />
</g>
</svg>
);
export const TableIcon = ({
size,
height = 24,
width = 22,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 22"
width={size || width}
strokeWidth="1.5"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5"
/>
</svg>
);
export const ArticleIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 20 20"
{...props}
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z"
/>
</svg>
);
export const MagazineIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 128 128"
{...props}
>
<path
fill="#bdbdbd"
d="M-125.7 124.54V11.79c29.36 1.85 58.81 1.91 88.18.19c1.77-.1 3.21 1.08 3.21 2.66v107.04c0 1.58-1.44 2.94-3.21 3.05a727 727 0 0 1-88.18-.19"
/>
<path
fill="#e0e0e0"
d="M-125.7 124.54V11.79c27.11-5.31 54.34-8.57 81.45-9.76c1.64-.07 2.96 1.15 2.96 2.73V111.8c0 1.58-1.33 2.9-2.96 2.98c-27.11 1.19-54.34 4.45-81.45 9.76"
/>
<g fill="#757575">
<path d="M-92.84 42.86c-7.46 1-14.91 2.16-22.36 3.47v-3.22c7.45-1.31 14.9-2.47 22.36-3.47zm-12.76-15.72c-3.2.51-6.4 1.04-9.6 1.6v-8.47c3.2-.56 6.4-1.1 9.6-1.6zm12.17-1.78c-3.2.43-6.4.9-9.6 1.39v-8.47c3.2-.49 6.4-.95 9.6-1.39zm12.17-1.52q-4.8.54-9.6 1.17v-8.47q4.8-.63 9.6-1.17zm12.17-1.23c-3.2.29-6.4.61-9.6.95v-8.47c3.2-.35 6.4-.66 9.6-.95zm17.21 5.12a548 548 0 0 0-63.31 7.42v-.81c21.09-3.72 42.23-6.19 63.31-7.42zm-32.67 14.08c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 40.77c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.11l-3.66.3v-3.22l3.66-.3z" />
<path d="M-65.38 39.87c-2.47.21-4.95.43-7.42.67v-3.22c2.47-.24 4.95-.46 7.42-.67zm10.32-.76c-2.02.13-4.05.27-6.07.42v-3.22c2.02-.15 4.05-.29 6.07-.42zm-49.89 14.57c-3.42.54-6.83 1.11-10.25 1.71v-3.22c3.41-.6 6.83-1.17 10.25-1.71zm9.51-1.4c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16z" />
<path d="M-84.55 50.87c-3.95.47-7.89.98-11.84 1.54v-3.22c3.95-.56 7.89-1.07 11.84-1.54z" />
<path d="M-75.06 49.82c-3.6.37-7.21.77-10.81 1.2V47.8c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3V45.8l3.66-.3z" />
<path d="M-61.13 48.59c-3.89.29-7.78.63-11.67 1.01v-3.22c3.89-.38 7.78-.71 11.67-1.01z" />
<path d="M-51.89 47.97c-3.08.18-6.16.39-9.25.62v-3.22c3.08-.23 6.17-.44 9.25-.62zm-49.5 14.22q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-95.44 61.33c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16zm10.89-1.4c-2.76.33-5.53.68-8.29 1.05v-3.22c2.76-.37 5.53-.72 8.29-1.05z" />
<path d="M-78.26 59.21c-2.54.27-5.07.56-7.61.87v-3.22c2.54-.31 5.07-.6 7.61-.87zm26.37 24.99q-12.075.705-24.18 1.95V55.76q12.09-1.245 24.18-1.95zm-38.55-14.48c-8.25 1.07-16.51 2.34-24.75 3.79v-3.22c8.24-1.45 16.5-2.72 24.75-3.79z" />
<path d="M-95.44 70.39c-1.31.18-2.63.37-3.94.56v-3.22c1.31-.19 2.63-.38 3.94-.56zm10.89-1.41c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-78.32 68.28c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-98.16 79.83c-1.72.25-3.44.51-5.17.77v-3.22c1.72-.27 3.44-.52 5.17-.77zm13.61-1.79q-5.445.645-10.89 1.41v-3.22c3.63-.51 7.26-.97 10.89-1.41z" />
<path d="M-80.46 77.57c-1.8.2-3.6.41-5.41.63v-3.22c1.8-.22 3.6-.43 5.41-.63zm-16.95 11.21c-5.93.85-11.86 1.79-17.79 2.84V88.4c5.92-1.04 11.85-1.99 17.79-2.84z" />
<path d="M-92.54 88.1c-2.28.31-4.56.62-6.84.96v-3.22q3.42-.495 6.84-.96zm7.99-1.01c-1.75.21-3.5.43-5.25.65v-3.22c1.75-.23 3.5-.44 5.25-.65z" />
<path d="M-78.32 86.39c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25zm14.22-1.95q-6.105.75-12.21 1.65V94.9q6.105-.9 12.21-1.65z" />
<path d="M-84.55 96.15c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 95.1c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3v-3.22l3.66-.3zm-5.64.47c-1.46.13-2.93.27-4.39.41v-3.22c1.46-.14 2.93-.28 4.39-.41z" />
<path d="M-51.89 93.25c-6 .35-12.01.8-18.01 1.35v-3.22c6.01-.55 12.01-1 18.01-1.35zm-43.56 13.36c-6.59.92-13.17 1.96-19.75 3.11v-3.22c6.58-1.16 13.16-2.2 19.75-3.11zm22.09-2.62c-3.48.34-6.96.72-10.44 1.13v-3.22c3.48-.41 6.96-.78 10.44-1.13zm-13.53 1.5c-2.18.27-4.36.55-6.55.85v-3.22c2.18-.3 4.36-.58 6.55-.85z" />
</g>
<path
fill="#eee"
d="M15.71 280.41V170.86h76.08a2.77 2.77 0 0 1 2.77 2.77v104.01a2.77 2.77 0 0 1-2.77 2.77z"
/>
<g fill="#757575">
<path d="M25.53 203.19h20.88v3.13H25.53zm0-22.19h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm-34.08 13.66h59.12v.79H25.53zm22.44 8.53h6.18v3.13h-6.18z" />
<path d="M52.92 203.19h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 203.19h6.93v3.13h-6.93zm10.9 0h5.67v3.13h-5.67zm-50.48 8.8h9.57v3.13h-9.57zm11.08 0h7.37v3.13h-7.37z" />
<path d="M43.1 211.99h11.05v3.13H43.1z" />
<path d="M52.92 211.99h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 211.99H76v3.13H65.11zm10.9 0h8.64v3.13h-8.64zm-50.48 8.8h12.89v3.13H25.53z" />
<path d="M36.61 220.79h7.37v3.13h-7.37zm9.8 0h7.74v3.13h-7.74z" />
<path d="M52.92 220.79h7.1v3.13h-7.1zm9.15 0h22.58v29.53H62.07zm-36.54 8.8h23.11v3.13H25.53z" />
<path d="M40.3 229.59h3.68v3.13H40.3zm7.67 0h6.18v3.13h-6.18z" />
<path d="M52.92 229.59h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53z" />
<path d="M36.61 238.39h4.82v3.13h-4.82zm7.37 0h10.17v3.13H43.98z" />
<path d="M52.92 238.39h5.04v3.13h-5.04zm-27.39 8.79h16.61v3.13H25.53z" />
<path d="M40.3 247.18h6.38v3.13H40.3zm8.95 0h4.9v3.13h-4.9z" />
<path d="M52.92 247.18h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53zm14.77 0h11.39v3.13H40.3z" />
<path d="M47.97 255.98h6.18v3.13h-6.18z" />
<path d="M52.92 255.98h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42zm-5.95 0h4.1v3.13h-4.1z" />
<path d="M67.82 255.98h16.82v3.13H67.82zm-42.29 8.8h18.44v3.13H25.53zm29.32 0h9.74v3.13h-9.74zm-9 0h6.11v3.13h-6.11z" />
</g>
<path
fill="#bdbdbd"
d="M16.62 124.27V14.04c30.52 2.2 61.18 2.27 91.71.21c1.68-.11 3.05 1.04 3.05 2.58v104.65c0 1.54-1.36 2.89-3.05 3a659 659 0 0 1-91.71-.21"
/>
<path
fill="#e0e0e0"
d="M16.62 124.25V14.02C44.36 7.91 72.21 3.9 99.95 2.03c1.53-.1 2.77 1.07 2.77 2.61v104.65c0 1.54-1.24 2.87-2.77 2.97c-27.74 1.87-55.59 5.88-83.33 11.99"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M28.75 49.34c20.6-4.08 41.25-7 61.84-8.74M28.75 63.23a565 565 0 0 1 26.45-4.6M28.75 77.11a565 565 0 0 1 26.45-4.6m-26.45 33.06c20.6-4.08 41.25-7 61.84-8.74M28.75 91a565 565 0 0 1 26.45-4.6"
/>
<path
fill="#616161"
d="M64.86 87.55a560 560 0 0 1 24.67-2.69c1.49-.13 2.69-1.44 2.69-2.94V54.54c0-1.5-1.21-2.61-2.69-2.48c-8.22.71-16.44 1.61-24.67 2.69c-1.49.2-2.7 1.58-2.7 3.07V85.2c.01 1.5 1.21 2.55 2.7 2.35m-34.4-52.14c2.03-.4 4.05-.78 6.08-1.15c1.49-.27 2.69-1.7 2.69-3.2v-7.02c0-1.5-1.21-2.49-2.69-2.22c-2.03.37-4.05.76-6.08 1.15c-1.49.29-2.69 1.75-2.69 3.24v7.02c-.01 1.5 1.2 2.47 2.69 2.18m15.96-2.88c2.03-.34 4.05-.66 6.08-.97c1.49-.23 2.7-1.62 2.7-3.12v-7.02c0-1.5-1.21-2.53-2.7-2.3c-2.03.31-4.06.64-6.08.97c-1.49.25-2.69 1.67-2.69 3.16v7.02c0 1.5 1.2 2.51 2.69 2.26m15.97-2.41c2.03-.28 4.06-.54 6.08-.8c1.49-.19 2.7-1.54 2.7-3.04v-7.02c0-1.5-1.21-2.57-2.7-2.38c-2.03.25-4.06.52-6.08.8c-1.49.2-2.7 1.59-2.7 3.08v7.02c.01 1.5 1.22 2.54 2.7 2.34"
/>
<path
fill="#e0e0e0"
d="M374.07 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M387.48 86.21h68.34m-68.34 15.26h29.23m-29.23 15.26h29.23M387.48 148h68.34m-68.34-16.01h29.23"
/>
<path
fill="#616161"
d="M427.38 134.75h27.26c1.64 0 2.98-1.33 2.98-2.98v-30.08c0-1.64-1.33-2.98-2.98-2.98h-27.26c-1.64 0-2.98 1.33-2.98 2.98v30.08a2.987 2.987 0 0 0 2.98 2.98m-38.01-63.47h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98m17.64 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71a2.987 2.987 0 0 0 2.98 2.98m17.65 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98"
/>
<path
fill="#bdbdbd"
d="M479.86 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
</svg>
);
export const StaticPageIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 2048 2048"
{...props}
>
<path
fill="currentColor"
d="M1755 512h-475V37zm37 128v1408H128V0h1024v640z"
/>
</svg>
);
export const MasterUsersIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M144 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160m368 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160M0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96H21.3C9.6 320 0 310.4 0 298.7M405.3 320h-.7c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7c58.9 0 106.7 47.8 106.7 106.7c0 11.8-9.6 21.3-21.3 21.3H405.4zM224 224a96 96 0 1 1 192 0a96 96 0 1 1-192 0m-96 261.3c0-73.6 59.7-133.3 133.3-133.3h117.3c73.7 0 133.4 59.7 133.4 133.3c0 14.7-11.9 26.7-26.7 26.7H154.6c-14.7 0-26.7-11.9-26.7-26.7z"
/>
</svg>
);
export const MasterRoleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M15 21h-2a2 2 0 0 1 0-4h2v-2h-2a4 4 0 0 0 0 8h2Zm8-2a4 4 0 0 1-4 4h-2v-2h2a2 2 0 0 0 0-4h-2v-2h2a4 4 0 0 1 4 4"
/>
<path
fill="currentColor"
d="M14 18h4v2h-4zm-7 1a6 6 0 0 1 .09-1H3v-1.4c0-2 4-3.1 6-3.1a8.6 8.6 0 0 1 1.35.125A5.95 5.95 0 0 1 13 13h5V4a2.006 2.006 0 0 0-2-2h-4.18a2.988 2.988 0 0 0-5.64 0H2a2.006 2.006 0 0 0-2 2v14a2.006 2.006 0 0 0 2 2h5.09A6 6 0 0 1 7 19M9 2a1 1 0 1 1-1 1a1.003 1.003 0 0 1 1-1m0 4a3 3 0 1 1-3 3a2.996 2.996 0 0 1 3-3"
/>
</svg>
);
export const MasterUserLevelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32S80 82.1 80 144s50.1 112 112 112m76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2M480 256c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96s43 96 96 96m48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4c24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48c0-61.9-50.1-112-112-112"
/>
</svg>
);
export const MasterCategoryIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 32 32"
{...props}
>
<path
fill="currentColor"
d="M14 25h14v2H14zm-6.83 1l-2.58 2.58L6 30l4-4l-4-4l-1.42 1.41zM14 15h14v2H14zm-6.83 1l-2.58 2.58L6 20l4-4l-4-4l-1.42 1.41zM14 5h14v2H14zM7.17 6L4.59 8.58L6 10l4-4l-4-4l-1.42 1.41z"
/>
</svg>
);
export const AddvertiseIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 8q-1.65 0-2.825 1.175T8 12q0 1.125.563 2.075t1.562 1.475q.4.2.563.587t-.013.788q-.175.35-.525.525t-.7 0q-1.575-.75-2.512-2.225T6 12q0-2.5 1.75-4.25T12 6q1.775 0 3.263.938T17.475 9.5q.15.35-.012.7t-.513.5q-.4.175-.8 0t-.6-.575q-.525-1-1.475-1.562T12 8m0-4Q8.65 4 6.325 6.325T4 12q0 3.15 2.075 5.4t5.2 2.55q.425.05.737.375t.288.75t-.313.7t-.712.25q-1.95-.125-3.638-.975t-2.95-2.213t-1.975-3.125T2 12q0-2.075.788-3.9t2.137-3.175T8.1 2.788T12 2q3.925 0 6.838 2.675t3.187 6.6q.05.4-.237.688t-.713.312t-.762-.275t-.388-.725q-.375-3-2.612-5.137T12 4m7.55 17.5l-3.3-3.275l-.75 2.275q-.125.35-.475.338t-.475-.363L12.275 12.9q-.1-.275.125-.5t.5-.125l7.575 2.275q.35.125.363.475t-.338.475l-2.275.75l3.3 3.3q.425.425.425.975t-.425.975t-.987.425t-.988-.425"
/>
</svg>
);
export const SuggestionsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17.175 14H7.5q-1.875 0-3.187-1.312T3 9.5t1.313-3.187T7.5 5q.425 0 .713.288T8.5 6t-.288.713T7.5 7q-1.05 0-1.775.725T5 9.5t.725 1.775T7.5 12h9.675L14.3 9.1q-.275-.275-.288-.687T14.3 7.7q.275-.275.7-.275t.7.275l4.6 4.6q.3.3.3.7t-.3.7l-4.6 4.6q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7z"
/>
</svg>
);
export const CommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M6.5 13.5h11v-1h-11zm0-3h11v-1h-11zm0-3h11v-1h-11zM4.616 17q-.691 0-1.153-.462T3 15.385V4.615q0-.69.463-1.153T4.615 3h14.77q.69 0 1.152.462T21 4.615v15.462L17.923 17z"
/>
</svg>
);

View File

@ -0,0 +1,83 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
export default function HeaderAfterSales() {
const services = [
{
image: "/after-sales.png",
title: "GARANSI KENDARAAN",
description:
"Jaecoo Indonesia berkomitmen seluruh pelanggan setia Jaecoo Indonesia dengan memberikan garansi kendaraan selama 6 tahun apabila terdapat cacat material atau kesalahan dari hasil kerja pabrik.",
},
{
image: "/after-sales.png",
title: "GARANSI MESIN",
description:
"Jaecoo Indonesia memberikan garansi mesin selama 10 tahun apabila terdapat cacat material atau kesalahan dari hasil kerja pabrik.",
},
{
image: "/after-sales.png",
title: "FREE BIAYA PERAWATAN",
description:
"Jaecoo Indonesia memberikan gratis biaya perawatan atau service di Dealer Resmi Jaecoo selama 4 tahun kepada seluruh pelanggan Jaecoo Indonesia.",
},
{
image: "/after-sales2.png",
title: "SPAREPART",
description:
"Jaecoo Indonesia menyediakan sparepart berkualitas terbaik dan orisinil dari pabrik Jaecoo Indonesia.",
},
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/header-as.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
<div className="w-full max-w-[1400px] mx-auto mt-12">
<h2 className="text-3xl font-bold text-black mb-8">
After Sales Services
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{services.map((item, index) => (
<div key={index} className="flex flex-col gap-4">
<div className="relative w-full h-80">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover "
/>
</div>
<h3 className="text-lg font-semibold text-[#1F6779]">
{item.title}
</h3>
<p className="text-[#1F6779] text-[18px]">
{item.description}
</p>
</div>
))}
</div>
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,78 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
const agents = [
{
name: "Johny Nugroho",
title: "Branch Manager Jaecoo Cihampelas Bandung",
image: "/johny.png",
},
{
name: "Basuki Pamungkas",
title: "Spv Jaecoo Cihampelas Bandung",
image: "/basuki.png",
},
{
name: "Deni Tihayar",
title: "Spv Jaecoo Cihampelas Bandung",
image: "/deni.png",
},
];
export default function Agent() {
return (
<section className="py-16 px-6 md:px-12 bg-[#f9f9f9] text-center mt-0">
<motion.h2
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-3xl md:text-6xl font-semibold text-gray-900 mb-2"
>
Our Teams
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="text-gray-600 mb-10 text-lg"
>
Temui anggota tim kami yang luar biasa
</motion.p>
<div className=" flex flex-row items-center justify-center gap-2">
{agents.map((agent, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: index * 0.2 + 0.3,
ease: "easeOut",
}}
viewport={{ once: true, amount: 0.3 }}
className="bg-white shadow-md px-2 py-4 gap-4 flex flex-col items-center h-[300px] w-[224px]"
>
<div className="relative w-28 h-36 mb-3">
<Image
src={agent.image}
alt={agent.name}
fill
className="rounded-full object-cover"
/>
</div>
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3>
<p className="text-xs text-gray-600 text-center mt-1">
{agent.title}
</p>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,67 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
const agents = [
{
name: "Johny Nugroho",
title: "Branch Manager Jaecoo Cihampelas Bandung",
image: "/johny.png",
},
{
name: "Basuki Pamungkas",
title: "Spv Jaecoo Cihampelas Bandung",
image: "/basuki.png",
},
{
name: "Deni Tihayar",
title: "Spv Jaecoo Cihampelas Bandung",
image: "/deni.png",
},
];
export default function BestAgent() {
return (
<section className="py-16 px-6 md:px-12 bg-[#f9f9f9] text-center mt-0">
<motion.h2
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-3xl md:text-4xl font-semibold text-gray-900 mb-10"
>
Our Teams
</motion.h2>
<div className=" flex flex-row items-center justify-center gap-2">
{agents.map((agent, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
duration: 0.6,
delay: index * 0.2 + 0.3,
ease: "easeOut",
}}
viewport={{ once: true, amount: 0.3 }}
className="bg-white shadow-md px-2 py-4 gap-4 flex flex-col items-center h-[300px] w-[224px]"
>
<div className="relative w-28 h-36 mb-3">
<Image
src={agent.image}
alt={agent.name}
fill
className="rounded-full object-cover"
/>
</div>
<h3 className="text-lg text-gray-900 text-center">{agent.name}</h3>
<p className="text-xs text-gray-600 text-center mt-1">
{agent.title}
</p>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,116 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect, useState } from "react";
const featuresJ8 = [
{
title: "ARDIS",
description:
"ARDIS (All-Road Drive Intelligent System) automatically adjusts power to each wheel, giving you better grip, stability, and control on any road.",
image: "/ex-j8.png",
},
{
title: "CDC Magnetic Suspension",
description:
"Equipped with real-time road condition detection, the J8 instantly adjusts shock absorbers to keep the vehicle stable, ensuring a smooth and controlled ride.",
image: "/ex-j8-2.png",
},
{
title: "2.0L Turbocharged Engine",
description:
"Equipped with a 2.0L turbo engine that delivers 183 kW and 385 Nm, the J8 offers strong performance and smooth control for any road.",
image: "/ex-j8-3.png",
},
{
title: "Waterfall Grille Design",
description:
"Features a bold, flowing design that captures attention at first glance. The cascading pattern blends elegance with energy, reflecting modern confidence while optimizing airflow for improved vehicle performance.",
image: "/ex-j8-4.png",
},
{
title: "LED Tech Headlamp",
description: "",
image: "/ex-j8-5.png",
},
{
title: "20-inch Alloy Wheels",
description:
"Striking 20-inch alloy wheels deliver a blend of style and durability. ",
image: "/ex-j8-6.png",
},
{
title: "Hidden door handles",
description:
"Seamlessly integrated into the vehicle body to reduce wind noise and drag.",
image: "/ex-j8-7.png",
},
{
title: "Strong Through Waistline",
description:
"The J8s exterior embodies bold simplicity with crisp, powerful lines and a golden-ratio silhouette.",
image: "/ex-j8-8.png",
},
];
export default function ExteriorJ8Awd() {
const [ref, inView] = useInView({ triggerOnce: true, threshold: 0.2 });
const [show, setShow] = useState(false);
useEffect(() => {
if (inView) {
setShow(true);
}
}, [inView]);
return (
<section
ref={ref}
className="py-10 px-4 sm:px-6 md:px-20 bg-white overflow-hidden"
>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
Teknologi dan Exterior
</motion.h2>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.2, duration: 0.8 }}
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5"
>
{featuresJ8.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.3 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</motion.div>
</section>
);
}

View File

@ -0,0 +1,141 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect, useState } from "react";
const featuresshs = [
{
title: "Rear view mirrors",
description:
"The mirrors on the pillars are a discreet but aesthetic design detail of the Jaecoo J7 SHS. Their contrasting inserts harmoniously resonate with other accent touches of the exterior.",
image: "/ex-shs3.png",
},
{
title: "Wheels 19”",
description:
"Built with a lightweight aluminum chassis, offering enhanced strength, durability, and improved performance for a superior driving experience.",
image: "/ex-shs4.png",
},
{
title: "Retractable handles",
description:
"The designers used a spectacular solution - door handles that automatically extend using an electric drive. Minimal force is required to open the door.",
image: "/ex-shs5.png",
},
{
title: "Rear Bumper Design",
description:
"Featuring refined lines and bold contours, the rear bumper enhances the vehicle's sporty and stylish character.",
image: "/ex-shs6.png",
},
];
export default function ExteriorShs() {
const [ref, inView] = useInView({ triggerOnce: true, threshold: 0.2 });
const [show, setShow] = useState(false);
useEffect(() => {
if (inView) {
setShow(true);
}
}, [inView]);
return (
<section
ref={ref}
className="py-10 px-4 sm:px-6 md:px-20 bg-white overflow-hidden"
>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
Teknologi dan Exterior
</motion.h2>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={show ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px] my-5"
>
<Image
src="/ex-shs.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-6 left-3 sm:left-3 md:left-6 max-w-5xl">
<h2 className="text-xl sm:text-xl font-semibold text-white">
5th generation 1.5t + 1dht
</h2>
<p className="text-xs sm:text-xs mt-2 text-gray-200">
Drive with peace of mind, protected by 7 strategically placed
airbags designed for maximum safety in every journey.
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={show ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]"
>
<Image
src="/ex-shs2.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-6 left-3 sm:left-3 md:left-6 max-w-5xl">
<h2 className="text-xl sm:text-xl font-semibold text-white">
IP68 protection hybrid battery
</h2>
<p className="text-xs sm:text-xs mt-2 text-gray-200">
Advanced Hybrid Battery Pack Designed for Durability and Performance
with IP68 Protection. Engineered for toughness, this hybrid battery
pack features IP68 protection and triple-layer safety against
damage.
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.2, duration: 0.8 }}
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5"
>
{featuresshs.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.3 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</motion.div>
</section>
);
}

View File

@ -0,0 +1,118 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect, useState } from "react";
const features = [
{
title: "REAR BUMPER DESIGN",
description:
"Featuring refined lines and bold contours, the rear bumper enhances the vehicles sporty and stylish character.",
image: "/pj7-1.png",
},
{
title: "PANORAMIC SUNROOF",
description:
"The Panoramic Sunroof transforms your journey, creating a brighter and more spacious atmosphere.",
image: "/pj7-2.png",
},
{
title: "REAR BUMPER DESIGN",
description:
"Featuring refined lines and bold contours, the rear bumper enhances the vehicles sporty and stylish character.",
image: "/pj7-3.png",
},
{
title: "NATURAL DAYLIGHT LED LIGHTING SYSTEM",
description:
"Illuminate your journey with the Natural Daylight LED Lighting System, designed to mimic the clarity and warmth of sunlight.",
image: "/pj7-4.png",
},
];
export default function Exterior() {
const [ref, inView] = useInView({ triggerOnce: true, threshold: 0.2 });
const [show, setShow] = useState(false);
useEffect(() => {
if (inView) {
setShow(true);
}
}, [inView]);
return (
<section
ref={ref}
className="py-10 px-4 sm:px-6 md:px-20 bg-white overflow-hidden"
>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span>{" "}
Teknologi dan Exterior
</motion.h2>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={show ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]"
>
<Image
src="/jj7-awd.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-6 left-3 sm:left-3 md:left-6 max-w-5xl">
<h2 className="text-xl sm:text-xl font-semibold text-white">
ALUMINIUM CHASSIS
</h2>
<p className="text-xs sm:text-xs mt-2 text-gray-200">
Built with a lightweight aluminum chassis, offering enhanced
strength, durability, and improved performance for a superior
driving experience.
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={show ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.2, duration: 0.8 }}
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5"
>
{features.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.3 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</motion.div>
</section>
);
}

View File

@ -0,0 +1,81 @@
"use client";
import Image from "next/image";
export default function FeaturesAndSpecificationsJ8() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span> Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
<Image
src="/fitur1.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-26 left-3 sm:left-10 md:left-26 max-w-xs bg-white/60 rounded-lg p-4">
<h2 className="text-xl sm:text-sm font-semibold text-black">
Lane Changing Assistance
</h2>
<p className="text-xs sm:text-xs mt-2 text-black">
Advanced safety feature that monitors surrounding traffic and
provides alerts or steering support to help ensure sasfe and
confident lane changes
</p>
</div>
</div>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px] my-20">
<Image
src="/awd-fitur8.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">
<div>
<p className="text-gray-500">Max Power</p>
<p className="font-bold">248ps</p>
</div>
<div>
<p className="text-gray-500">AWD Technology</p>
<p className="font-bold">ARDIS</p>
</div>
<div>
<p className="text-gray-500">Suspension</p>
<p className="font-bold">CDC Magnetic</p>
</div>
<div>
<p className="text-gray-500">ADAS</p>
<p className="font-bold">19 adas</p>
</div>
<div>
<p className="text-gray-500">Engine</p>
<p className="font-bold">2.0TGDI</p>
</div>
<div>
<p className="text-gray-500">0-100km-h</p>
<p className="font-bold">8.8s</p>
</div>
<div>
<p className="text-gray-500">airbag</p>
<p className="font-bold">10 airbags</p>
</div>
<div>
<p className="text-gray-500">Torque</p>
<p className="font-bold">385N.m</p>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,76 @@
"use client";
import Image from "next/image";
export default function FeaturesAndSpecificationsShs() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span> Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
<Image
src="/fitur1.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-26 left-3 sm:left-10 md:left-26 max-w-xs bg-white/60 rounded-lg p-4">
<h2 className="text-xl sm:text-sm font-semibold text-black">
Lane Changing Assistance
</h2>
<p className="text-xs sm:text-xs mt-2 text-black">
Advanced safety feature that monitors surrounding traffic and
provides alerts or steering support to help ensure sasfe and
confident lane changes
</p>
</div>
</div>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px] my-20">
<Image
src="/fitur2.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">
<div>
<p className="text-gray-500">Max Range</p>
<p className="font-bold">1,200km</p>
</div>
<div>
<p className="text-gray-500">Power Train</p>
<p className="font-bold">1.5T+1DHT</p>
</div>
<div>
<p className="text-gray-500">Pure EV Mode Range</p>
<p className="font-bold">WLTP 90km</p>
</div>
<div>
<p className="text-gray-500">ADAS</p>
<p className="font-bold">19 ADAS</p>
</div>
<div>
<p className="text-gray-500">Battery Capacity</p>
<p className="font-bold">18.3kWh</p>
</div>
<div>
<p className="text-gray-500">0-100km-h</p>
<p className="font-bold">8,5s</p>
</div>
<div>
<p className="text-gray-500">Airbag</p>
<p className="font-bold">8 airbags</p>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import Image from "next/image";
export default function FeaturesAndSpecifications() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span> Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
<Image
src="/fitur1.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute bottom-26 left-3 sm:left-10 md:left-26 max-w-xs bg-white/60 rounded-lg p-4">
<h2 className="text-xl sm:text-sm font-semibold text-black">
Lane Changing Assistance
</h2>
<p className="text-xs sm:text-xs mt-2 text-black">
Advanced safety feature that monitors surrounding traffic and
provides alerts or steering support to help ensure sasfe and
confident lane changes
</p>
</div>
</div>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px] my-20">
<Image
src="/fitur2.png"
alt="Aluminium Chassis"
fill
className="object-cover"
sizes="100vw"
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">
<div>
<p className="text-gray-500">Max Power</p>
<p className="font-bold">136,5kw/183hp</p>
</div>
<div>
<p className="text-gray-500">Power Train</p>
<p className="font-bold">1.6T+7DHT</p>
</div>
<div>
<p className="text-gray-500">Torque</p>
<p className="font-bold">275N.m</p>
</div>
<div>
<p className="text-gray-500">Sensor</p>
<p className="font-bold">8 Sensor</p>
</div>
<div>
<p className="text-gray-500">Max Speed</p>
<p className="font-bold">180km/h</p>
</div>
<div>
<p className="text-gray-500">0-100km-h</p>
<p className="font-bold">9,2s</p>
</div>
<div>
<p className="text-gray-500">Airbag</p>
<p className="font-bold">8 airbags</p>
</div>
<div>
<p className="text-gray-500">ADAS</p>
<p className="font-bold">19 adas</p>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,153 @@
import Image from "next/image";
export default function Footer() {
return (
<footer className="bg-black text-[#c7dbe3] px-6 md:px-20 py-16">
<div className="flex flex-col md:flex-row gap-10">
<div className="w-full md:w-4/12">
<Image
src="/masjaecoo.png"
alt="Jaecoo"
width={300}
height={200}
className="ml-4"
/>
<div className="flex gap-4 mt-6 ml-24 md:ml-20 text-xl text-[#c7dbe3]">
<div className="hover:text-white cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
// stroke-width="1.5"
>
<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>
<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 className="md:w-8/12 ">
<div className="flex flex-wrap md:flex-row gap-10 md:gap-28">
<div>
<h4 className="font-semibold text-white mb-4">ABOUT</h4>
<ul className="space-y-4 text-sm">
<li>
<a href="#">Partnership</a>
</li>
<li>
<a href="#">Terms of Use</a>
</li>
<li>
<a href="#">Privacy</a>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-white mb-4">PRODUCT</h4>
<ul className="space-y-4 text-sm">
<li>
<a href="#">About</a>
</li>
<li>
<a href="#">Features</a>
</li>
<li>
<a href="#">Support</a>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-white mb-4">RESOURCES</h4>
<ul className="space-y-4 text-sm">
<li>
<a href="#">Career</a>
</li>
<li>
<a href="#">Blog</a>
</li>
<li>
<a href="#">Legal</a>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-white mb-4">CONTACT</h4>
<ul className="space-y-4 text-sm">
<li>
<a href="https://jaecoo.com" target="_blank" rel="noreferrer">
jaecoo.com
</a>
</li>
<li>0851-1234-567</li>
<li>
<p className="font-semibold text-white">Jaecoo Bandung</p>
<p className="w-8/12">
Jaecoo Cihampelas Bandung, Jl. Cihampelas No. 264-268,
<br />
Bandung, Jawa Barat 40131
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,67 @@
"use client";
import Image from "next/image";
import { useState } from "react";
import { motion } from "framer-motion";
const images = ["/g1.png", "/g2.png", "/g3.png", "/g4.png"];
export default function Galeri() {
const [consent, setConsent] = useState(false);
return (
<section className="px-4 py-12 md:px-20 bg-white">
<motion.h2
className="text-3xl font-bold mb-8 text-black"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
viewport={{ once: true }}
>
Galeri Kami
</motion.h2>
<div className="flex flex-col gap-8 mb-16">
<div className="flex justify-start gap-8 flex-wrap">
{[images[0], images[1]].map((src, index) => (
<motion.div
key={index}
className="relative w-[400px] h-[250px] overflow-hidden"
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
>
<Image
src={src}
alt={`Galeri ${index + 1}`}
fill
className="object-cover object-center"
/>
</motion.div>
))}
</div>
<div className="flex justify-end gap-8 flex-wrap">
{[images[2], images[3]].map((src, index) => (
<motion.div
key={index + 2}
className="relative w-[400px] h-[250px] overflow-hidden"
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
viewport={{ once: true }}
>
<Image
src={src}
alt={`Galeri ${index + 3}`}
fill
className="object-cover object-center"
/>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { ChevronLeft, ChevronRight } from "lucide-react";
const imagesPerPage = 6;
const galleryImages = [
"/gl1.png",
"/gl2-new.png",
"/gl3.png",
"/gl4.png",
"/gl5.png",
"/gl6.png",
"/gl7.png",
"/gl8.png",
"/gl9.png",
];
export default function GallerySection() {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(galleryImages.length / imagesPerPage);
const paginatedImages = galleryImages.slice(
(currentPage - 1) * imagesPerPage,
currentPage * imagesPerPage
);
return (
<section className="py-16 px-4 max-w-[1400px] mx-auto">
<h2 className="text-4xl font-bold mb-8">Galeri Kami</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{paginatedImages.map((img, index) => (
<div key={index} className="relative w-full aspect-[3/2]">
<Image
src={img}
alt={`gallery-${index}`}
fill
className="object-cover"
/>
</div>
))}
</div>
<div className="flex items-center justify-center gap-2 mt-10">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="p-2 rounded-md hover:bg-gray-200 disabled:opacity-30"
>
<ChevronLeft />
</button>
{[...Array(totalPages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`w-8 h-8 rounded-md border text-sm ${
currentPage === i + 1
? "bg-[#1F6779] text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
{i + 1}
</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>
</section>
);
}

View File

@ -0,0 +1,143 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
export default function HeaderAbout() {
return (
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6 mb-24"
>
<h2 className="text-4xl font-bold mb-1">
Mengenal Lebih Dekat Dealer Resmi{" "}
<span className="text-[#1F6779]">Jaecoo</span>
</h2>
<p className="text-lg">
Komitmen kami adalah memberikan layanan terbaik dan pengalaman premium
di setiap kunjungan Anda.
</p>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[500px] overflow-hidden">
<Image
src="/journey.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
</motion.div>
<div className="max-w-[1400px] mx-auto flex flex-col lg:flex-row gap-10 my-10">
<motion.div
className="relative w-full lg:w-[536px] h-[300px] sm:h-[400px] lg:h-[576px] overflow-hidden"
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
viewport={{ once: true }}
>
<Image
src="/mas-group.png"
alt="Dealer Jaecoo"
fill
className="object-cover object-center"
sizes="(max-width: 768px) 100vw, 536px"
/>
</motion.div>
<motion.div
className="w-full lg:w-8/12 space-y-10 "
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
viewport={{ once: true }}
>
<h2 className="text-2xl sm:text-3xl font-bold text-black">
<span className="text-[#1F6779]">Mas </span>Group
</h2>
<div className="flex flex-col gap-8 mt-5">
<p className="text-black leading-relaxed text-xl">
 MAS Group began its journey as a vehicle rental service provider,
supporting Chevron Groups oil and gas exploration vendors in Riau
Province. In 2002, with the entry of Ford Motor Company into the
Indonesian market, we were appointed as an authorized Ford dealer
for the Riau region.
</p>
<p className="text-black leading-relaxed text-xl">
Thanks to our teams extensive experience and the strength of our
product offerings, we successfully secured large fleet contracts
and were consistently recognized by Ford Motor Indonesia as one of
its top-performing dealers over several consecutive years.
</p>
<p className="text-black leading-relaxed text-xl">
Building on this success, we expanded our automotive portfolio to
include Mercedes-Benz trucks dealership operations and broadened
our car rental services to meet the increasing demand from sectors
such as coal mining, oil and gas, palm oil plantations, and
logistics.
</p>
<p className="text-black leading-relaxed text-xl">
Today, we are proud to be a major EV dealer in Indonesia,
representing several leading brands: Chery & Tiggo ,Great Wall
Motors (Tank, Haval, Ora), MG, Omoda & Jaecoo and Lepas.
</p>
</div>
</motion.div>
</div>
<div className="max-w-[1400px] mx-auto flex flex-col lg:flex-row gap-10 mt-24">
<motion.div
className="w-full lg:w-8/12 space-y-6 "
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
viewport={{ once: true }}
>
<h2 className="text-2xl sm:text-4xl font-bold text-black">
Mengenal Lebih Dekat Dealer Resmi{" "}
<span className="text-[#1F6779]">Jaecoo</span>
</h2>
<div>
<p className="text-gray-600 leading-relaxed text-xl">
Dealer resmi Jaecoo sejak 2023, berlokasi di pusat Bandung. Kami
melayani penjualan, servis, serta test drive dengan fasilitas
showroom modern dan teknisi bersertifikat.
</p>
</div>
<div className="text-gray-700 text-xl space-y-2">
<p>
<strong>Alamat:</strong> Jaecoo Cihampelas Bandung, Jl. Cihampelas
No. 264-268, Bandung, Jawa Barat 40131
</p>
<p>
<strong>Telepon:</strong> 021-12345678
</p>
<p>
<strong>Email:</strong> info@dealerjaecoo.id
</p>
</div>
</motion.div>
<motion.div
className="relative w-full lg:w-[536px] h-[300px] sm:h-[400px] lg:h-[576px] overflow-hidden"
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
viewport={{ once: true }}
>
<Image
src="/dealer.png"
alt="Dealer Jaecoo"
fill
className="object-cover object-center"
sizes="(max-width: 768px) 100vw, 536px"
/>
</motion.div>
</div>
</section>
);
}

View File

@ -0,0 +1,115 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { motion } from "framer-motion";
import { useState } from "react";
export default function HeaderAfterSalesServices() {
const cars = [
{
title: "JAECOO J7 AWD",
image: "/j7-awd-nobg.png",
price: "Rp 549.000.000",
oldPrice: "Rp 544.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J7 SHS",
image: "/j7-shs-nobg.png",
price: "Rp 599.000.000",
oldPrice: "Rp 594.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J8 AWD",
image: "/j8-awd-nobg.png",
price: "Rp 812.000.000",
oldPrice: "Rp 807.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<h2 className="text-4xl font-bold mb-1">
Layanan Konsumen After Sales
</h2>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/layanan-sales.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6 mt-20"
>
<div className="w-full max-w-[1400px] mx-auto grid grid-cols-1 lg:grid-cols-[3fr_1fr] gap-6">
<div className="relative h-[300px] sm:h-[400px] md:h-[500px] w-full overflow-hidden ">
<Image
src="/further.png"
alt="Banner After Sales"
fill
className="rounded-md object-cover"
priority
/>
</div>
<div className="flex flex-col items-center lg:items-start justify-center text-center lg:text-left gap-4 px-4 py-6">
<Image
src="/jhony.png"
alt="Johny"
width={150}
height={150}
className="rounded-md object-cover"
/>
<div>
<h3 className="text-xl font-semibold">Johny</h3>
<p className="text-sm text-gray-600 mt-1">
Silahkan Hubungi Johny untuk Layanan Konsumen After Sales
Jaecoo Cihampelas Bandung
</p>
</div>
<Button
asChild
className="bg-transparent hover:bg-green-600 mt-4 w-full border border-[#BCD4DF] text-[#1F6779]"
size={"lg"}
>
<a
href="https://wa.me/62XXXXXXXXXX"
target="_blank"
rel="noopener noreferrer"
>
Whatsapp
</a>
</Button>
</div>
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,26 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
export default function HeaderItems() {
return (
<section className="py-10 px-6 md:px-10 bg-white">
<motion.div
className="flex flex-col items-center gap-10"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<div className="relative w-full h-[640px] overflow-hidden p-5">
<Image
src={"/service.png"}
alt="Service Header"
fill
className="object-cover"
sizes="100vw"
/>
</div>
</motion.div>
</section>
);
}

View File

@ -0,0 +1,273 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { motion } from "framer-motion";
import { useState } from "react";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
export default function HeaderPriceInformation() {
const [open, setOpen] = useState(false);
const cars = [
{
title: "JAECOO J7 AWD",
image: "/j7-awd-nobg.png",
price: "Rp 549.000.000",
oldPrice: "Rp 544.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J7 SHS",
image: "/j7-shs-nobg.png",
price: "Rp 599.000.000",
oldPrice: "Rp 594.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J8 AWD",
image: "/j8-awd-nobg.png",
price: "Rp 812.000.000",
oldPrice: "Rp 807.000.000",
capacity: "18.3kWh",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<h2 className="text-4xl font-bold mb-1">
Harga Terbaik Di Dealer Resmi Kami
</h2>
<p className="text-lg">
Dapatkan penawaran terbaik di dealer resmi kami untuk pengalaman
berkendara yang tak terlupakan!
</p>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/product1.jpg"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-[1400px] mt-10">
{cars.map((car, idx) => (
<div
key={idx}
className="relative rounded-2xl border p-6 pt-10 flex flex-col items-center shadow-sm"
>
<span className="absolute top-4 left-4 bg-[#1F6779] text-white text-sm px-3 py-1 rounded-md z-10">
NEW OFFER
</span>
<h3 className="text-3xl font-bold text-center mt-5">
{car.title}
</h3>
<div className="relative w-full h-48 my-4">
<Image
src={car.image}
alt={car.title}
fill
className="object-contain"
/>
</div>
<div className="flex items-center justify-center border rounded-lg p-3 w-full text-center mb-2 gap-3">
<p className="text-xs text-[#1F6779]">START FROM</p>
<p className="text-2xl font-bold text-[#1F6779]">
{car.price}
</p>
<div className="text-[#1F6779]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="13"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m0 26a12 12 0 1 1 12-12a12 12 0 0 1-12 12"
/>
<path
fill="currentColor"
d="M15 8h2v11h-2zm1 14a1.5 1.5 0 1 0 1.5 1.5A1.5 1.5 0 0 0 16 22"
/>
</svg>
</div>
</div>
<p className="text-[15px] text-black text-start mb-4">
*Save Rp 5.000.000 on the previous driveway price of{" "}
{car.oldPrice}. Offer ends 31st August 2025.
</p>
<div className="grid grid-cols-2 gap-2 w-full text-lg text-center mb-4">
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 36 36"
>
<path
fill="currentColor"
d="M22 4V2.62a.6.6 0 0 0-.58-.62h-6.84a.6.6 0 0 0-.58.62V4h-4a1.09 1.09 0 0 0-1 1.07v28a1 1 0 0 0 1 .93h16a1 1 0 0 0 1-.94v-28A1.09 1.09 0 0 0 26 4Zm-1.74 21.44a1.2 1.2 0 0 1-2.15 1.07l-5.46-10.95l6 1l-2.29-4a1.2 1.2 0 1 1 2.08-1.2l4.83 8.37l-6.37-1.03Z"
className="clr-i-solid clr-i-solid-path-1"
/>
<path fill="none" d="M0 0h36v36H0z" />
</svg>
<div className="flex flex-col items-center">
<span className="font-bold">{car.capacity}</span>{" "}
<span className="text-sm">Battery Capacity</span>
</div>
</div>
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m5.954-9.25h-3.049a3 3 0 0 1-.803 1.39l1.524 2.64a6 6 0 0 0 2.328-4.03m-3.626 4.782l-1.525-2.64a3 3 0 0 1-1.606 0l-1.525 2.64A6 6 0 0 0 12 18c.825 0 1.612-.167 2.328-.468m-5.954-.751l1.524-2.64a3 3 0 0 1-.804-1.391H6.046a6 6 0 0 0 2.328 4.03m9.58-5.531h-3.049a3 3 0 0 0-.803-1.39l1.524-2.64a6 6 0 0 1 2.328 4.03m-3.626-4.782A6 6 0 0 0 12 6c-.825 0-1.612.167-2.328.468l1.525 2.64a3 3 0 0 1 1.606 0zM9.898 9.86L8.374 7.22a6 6 0 0 0-2.328 4.03h3.049c.138-.535.42-1.013.803-1.39"
// clip-rule="evenodd"
/>
</svg>
<div className="flex flex-col items-center justify-center text-center">
<span className="font-bold">{car.wheels}</span>{" "}
<span className="text-sm">Alloy Wheels</span>
</div>
</div>
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="m71.47 18.38l-.01.01c-6.58-.1-14.25.79-21.52 2.41c-8.31 1.84-16.18 4.69-21.3 7.56c-2.57 1.44-4.42 2.9-5.24 3.8l25.86 90.54c7.22-9.1 15.41-16.6 23.75-22.2c9.69-6.44 19.19-10.67 27.89-12.47c0-13.14-.3-25.92-1.8-36.76c-1.9-13.05-5.6-23.03-11.5-28.91c-1.3-1.35-6.28-3.44-13.39-3.88c-.89 0-1.81-.1-2.74-.1m29.03 92.12c-6.7.4-14.2 3.5-21.1 8.7c-13.68 10.3-24.04 28.7-24.34 40.2l45.74 240.3c7.6-9.5 19.2-15.7 32.2-15.7c11.5 0 22 4.9 29.5 12.7c5.1-1.1 10.5-2.2 16.4-3.3c1.5-.3 3.1-.5 4.7-.8c-13.5-92.5-35.3-199.6-65.2-275.3c-5.2-4.8-10.3-6.7-15.6-6.8zm283 39.5l-53.6 167.4l17.2 5.4l24-75.1l117.1 37.5l5.4-17.2l-117-37.4l24.1-75.2zm-38.7 245.3c-21.5.1-46.3 1.4-71 3.7c-33 2.9-66 7.4-91.6 12.1c-3.5.6-6.8 1.3-10 1.9q1.8 5.7 1.8 12c0 22.5-18.5 41-41 41c-5.6 0-11-1.2-15.9-3.2c-3.1 8.9-5.4 17.6-6.7 24.2H398c5 0 7.7-1.8 10.7-6.4c3.1-4.7 5.4-12.4 6.3-21.5c1.9-18.1-2.1-41.2-9.1-55.1c.3.5-2.8-2.5-10.2-4.4s-18.1-3.3-30.7-3.9c-6.3-.3-13.1-.4-20.2-.4M133 402c-12.8 0-23 10.2-23 23s10.2 23 23 23s23-10.2 23-23s-10.2-23-23-23"
/>
</svg>
<div className="flex flex-col items-center justify-center text-center ml-5">
<span className="font-bold">{car.seats}</span>{" "}
<span className="text-sm"> Seats</span>
</div>
</div>
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 36 36"
>
<path
fill="currentColor"
d="M32.5 3h-29A1.5 1.5 0 0 0 2 4.5v21A1.5 1.5 0 0 0 3.5 27h29a1.5 1.5 0 0 0 1.5-1.5v-21A1.5 1.5 0 0 0 32.5 3M32 25H4V5h28Z"
className="clr-i-outline clr-i-outline-path-1"
/>
<path
fill="currentColor"
d="M7.7 8.76h20.43l1.81-1.6H6.1V23h1.6z"
className="clr-i-outline clr-i-outline-path-2"
/>
<path
fill="currentColor"
d="M26 32h-1.74a3.6 3.6 0 0 1-1.5-2.52v-1.35h-1.52v1.37a4.2 4.2 0 0 0 .93 2.5h-8.34a4.2 4.2 0 0 0 .93-2.52v-1.35h-1.52v1.37a3.6 3.6 0 0 1-1.5 2.5h-1.8a1 1 0 1 0 0 2h16.12a.92.92 0 0 0 1-1A1 1 0 0 0 26 32"
className="clr-i-outline clr-i-outline-path-3"
/>
<path fill="none" d="M0 0h36v36H0z" />
</svg>
<div className="flex flex-col items-center justify-center text-center ml-5">
<span className="font-bold">{car.display}</span>{" "}
<span className="text-sm"> Display</span>
</div>
</div>
</div>
<div className="flex gap-2 w-full ">
<Link href={"/product"} className="w-full">
<Button
className=" w-full border-[#1F6779] text-lg p-6"
variant="outline"
>
View Specs
</Button>
</Link>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[50px] p-6 hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
))}
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,179 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { motion } from "framer-motion";
import { useState } from "react";
import { Download } from "lucide-react";
export default function HeaderProductJ7Awd() {
const [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = [
"/jj7-cars.png", // index 0
"/green-j7-awd.png", // index 1
"/black-j7-awd.png", // index 2
"/white-j7-awd.png", // index 3
];
const gradients = [
"linear-gradient(to bottom, #B0B5C2, #B0B5C2)", // Hijau
"linear-gradient(to bottom, #5D6B4F, #5D6B4F)", // Silver
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Putih
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/product1.jpg"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
{/* Tombol di dalam gambar, posisi bawah tengah */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
<DialogTrigger asChild>
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
BROSUR
</Button>
</DialogTrigger>
<DialogContent className=" w-full p-0 overflow-hidden">
{/* Download Button */}
<div className="flex justify-end p-4 bg-white z-50">
<a
href={downloadLink}
target="_blank"
rel="noopener noreferrer"
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3"
>
<Download size={18} />
</a>
</div>
{/* Iframe Preview */}
<iframe
src={embedLink}
className="w-full h-[70vh] border-t"
allow="autoplay"
></iframe>
</DialogContent>
</Dialog>
{/* Trigger untuk modal */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</motion.div>
</section>
{/* Section warna */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[740px] overflow-hidden"
>
<Image
src={images[selectedColorIndex]}
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
<motion.div
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
className="absolute top-1/2 left-5 md:left-56 transform -translate-y-1/2 flex flex-col gap-4 z-10"
>
{gradients.map((bg, index) => (
<motion.button
key={index}
onClick={() => setSelectedColorIndex(index)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className={`w-6 h-6 rounded-full border-2 ${
selectedColorIndex === index ? "border-black" : "border-white"
} shadow-md hover:cursor-pointer`}
style={{ background: bg }}
aria-label={`Pilih warna ${index + 1}`}
/>
))}
</motion.div>
</motion.div>
</>
);
}

View File

@ -0,0 +1,179 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { motion } from "framer-motion";
import { useState } from "react";
import { Download } from "lucide-react";
export default function HeaderProductJ7Shs() {
const [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = [
"/jj7-blue.png", // index 0
"/jj7-white.png", // index 1
"/jj7-silver.png", // index 2
"/jj7-black.png", // index 3
];
const gradients = [
"linear-gradient(to bottom, #527D97, #527D97)", // Hijau
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Silver
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)", // Putih
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[700px] overflow-hidden">
<Image
src="/shs-header.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
{/* Tombol di dalam gambar, posisi bawah tengah */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
<DialogTrigger asChild>
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
BROSUR
</Button>
</DialogTrigger>
<DialogContent className=" w-full p-0 overflow-hidden">
{/* Download Button */}
<div className="flex justify-end p-4 bg-white z-50">
<a
href={downloadLink}
target="_blank"
rel="noopener noreferrer"
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3"
>
<Download size={18} />
</a>
</div>
{/* Iframe Preview */}
<iframe
src={embedLink}
className="w-full h-[70vh] border-t"
allow="autoplay"
></iframe>
</DialogContent>
</Dialog>
{/* Trigger untuk modal */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</motion.div>
</section>
{/* Section warna */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[740px] overflow-hidden"
>
<Image
src={images[selectedColorIndex]}
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
<motion.div
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
className="absolute top-1/2 left-5 md:left-56 transform -translate-y-1/2 flex flex-col gap-4 z-10"
>
{gradients.map((bg, index) => (
<motion.button
key={index}
onClick={() => setSelectedColorIndex(index)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className={`w-6 h-6 rounded-full border-2 ${
selectedColorIndex === index ? "border-black" : "border-white"
} shadow-md hover:cursor-pointer`}
style={{ background: bg }}
aria-label={`Pilih warna ${index + 1}`}
/>
))}
</motion.div>
</motion.div>
</>
);
}

View File

@ -0,0 +1,179 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { motion } from "framer-motion";
import { useState } from "react";
import { Download } from "lucide-react";
export default function HeaderProductJ8Awd() {
const [open, setOpen] = useState(false);
const [selectedColorIndex, setSelectedColorIndex] = useState(0);
const [openBrosur, setOpenBrosur] = useState(false);
const fileId = "1Nici3bdjUG524sUYQgHfbeO63xW6f1_o";
const fileLink = `https://drive.google.com/file/d/${fileId}/view`;
const embedLink = `https://drive.google.com/file/d/${fileId}/preview`;
const downloadLink = `https://drive.google.com/uc?export=download&id=${fileId}`;
const images = [
"/green.png", // index 0
"/silver.png", // index 1
"/white.png", // index 3
"/black.png", // index 2
];
const gradients = [
"linear-gradient(to bottom, #527D97, #1F6779)", // Hijau
"linear-gradient(to bottom, #FFFFFF, #FFFFFF)", // Silver
"linear-gradient(to bottom, #E1ECF4, #FFFFFF)", // Putih
"linear-gradient(to bottom, #1A1A1A, #3A3A3A)", // Hitam
];
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[700px] overflow-hidden">
<Image
src="/awd-8.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
{/* Tombol di dalam gambar, posisi bawah tengah */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-3">
<Dialog open={openBrosur} onOpenChange={setOpenBrosur}>
<DialogTrigger asChild>
<Button className="bg-white text-black border w-[100px] h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:bg-amber-50 hover:cursor-pointer">
BROSUR
</Button>
</DialogTrigger>
<DialogContent className=" w-full p-0 overflow-hidden">
{/* Download Button */}
<div className="flex justify-end p-4 bg-white z-50">
<a
href={downloadLink}
target="_blank"
rel="noopener noreferrer"
className="absolute top-2 right-3 z-50 bg-black text-white p-2 rounded hover:bg-gray-800 mb-3"
>
<Download size={18} />
</a>
</div>
{/* Iframe Preview */}
<iframe
src={embedLink}
className="w-full h-[70vh] border-t"
allow="autoplay"
></iframe>
</DialogContent>
</Dialog>
{/* Trigger untuk modal */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-xl hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</motion.div>
</section>
{/* Section warna */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.8 }}
className="relative w-full h-[300px] sm:h-[400px] md:h-[740px] overflow-hidden"
>
<Image
src={images[selectedColorIndex]}
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
<motion.div
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
className="absolute top-1/2 left-5 md:left-56 transform -translate-y-1/2 flex flex-col gap-4 z-10"
>
{gradients.map((bg, index) => (
<motion.button
key={index}
onClick={() => setSelectedColorIndex(index)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className={`w-6 h-6 rounded-full border-2 ${
selectedColorIndex === index ? "border-black" : "border-white"
} shadow-md hover:cursor-pointer`}
style={{ background: bg }}
aria-label={`Pilih warna ${index + 1}`}
/>
))}
</motion.div>
</motion.div>
</>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import Image from "next/image";
import { Button } from "../ui/button";
import { motion } from "framer-motion";
import { useState } from "react";
export default function HeaderSalesServices() {
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<h2 className="text-4xl font-bold mb-1">
Layanan Konsumen After Sales
</h2>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/layanan-sales.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6 mt-20"
>
<div className="w-full max-w-[1400px] mx-auto grid grid-cols-1 lg:grid-cols-[3fr_1fr] gap-6">
{/* Left: Image Banner */}
<div className="relative h-[300px] sm:h-[400px] md:h-[500px] w-full overflow-hidden ">
<Image
src="/further.png"
alt="Banner After Sales"
fill
className="rounded-md object-cover"
priority
/>
</div>
{/* Right: Johny Info Card */}
<div className="flex flex-col items-center lg:items-start justify-center text-center lg:text-left gap-4 px-4 py-6">
<Image
src="/jhony.png" // Ganti ini sesuai path foto Johny yang benar
alt="Johny"
width={150}
height={150}
className="rounded-md object-cover"
/>
<div>
<h3 className="text-xl font-semibold">Johny</h3>
<p className="text-sm text-gray-600 mt-1">
Silahkan Hubungi Johny untuk Layanan Konsumen After Sales
Jaecoo Cihampelas Bandung
</p>
</div>
<Button
asChild
className="bg-transparent hover:bg-green-600 mt-4 w-full border border-[#BCD4DF] text-[#1F6779]"
size={"lg"}
>
<a
href="https://wa.me/62XXXXXXXXXX" // Ganti dengan nomor WA yang benar
target="_blank"
rel="noopener noreferrer"
>
Whatsapp
</a>
</Button>
</div>
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,147 @@
"use client";
import Image from "next/image";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { useState } from "react";
import Autoplay from "embla-carousel-autoplay"; // ✅ Import plugin autoplay
import { useRef } from "react";
const heroImages = ["/Hero.png", "/hero-bdg2.png", "/hero-bdg3.png"];
export default function Header() {
const [open, setOpen] = useState(false);
// ✅ Gunakan useRef untuk plugin autoplay
const plugin = useRef(Autoplay({ delay: 4000, stopOnInteraction: false }));
return (
<section className="relative w-full overflow-hidden bg-white">
<Carousel
className="w-full relative"
plugins={[plugin.current]} // ✅ Tambahkan plugin di sini
>
<CarouselContent>
{heroImages.map((img, index) => (
<CarouselItem key={index}>
<div className="relative w-full h-[810px]">
<Image
src={img}
alt={`JAECOO Image ${index + 1}`}
width={1400}
height={810}
className="object-cover w-full h-full"
/>
{index === 0 && (
<div className="absolute inset-0 flex flex-col justify-center items-start px-6 md:px-28 z-10">
<motion.h1
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-3xl sm:text-4xl md:text-5xl font-bold text-black mb-4"
>
JAECOO J7 AWD
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.9,
ease: "easeOut",
delay: 0.2,
}}
className="text-lg text-black mb-6"
>
DELICATE OFF-ROAD SUV
</motion.p>
<motion.div
className="flex items-center gap-4"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, ease: "easeOut", delay: 0.4 }}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-2xl text-center mb-4">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
<Link href={"/product"}>
<Button
variant="outline"
className="rounded-full border-black text-black px-6 py-2 hover:cursor-pointer hover:bg-amber-50"
>
EXPLORE
</Button>
</Link>
</motion.div>
</div>
)}
{/* <CarouselPrevious className="absolute left-6 top-1/2 -translate-y-1/2 z-20 border border-[#155B6E] text-[#155B6E] bg-white" />
<CarouselNext className="absolute right-6 top-1/2 -translate-y-1/2 z-20 border border-[#155B6E] text-[#155B6E] bg-white" /> */}
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</section>
);
}

View File

@ -0,0 +1,117 @@
import Image from "next/image";
export default function Help() {
return (
<section className="max-w-[1400px] mx-auto bg-white pt-16 px-4 sm:px-6 lg:px-10">
<h2 className="text-2xl sm:text-3xl font-bold text-center mb-6">
Need More Help ?
</h2>
<h2 className="text-2xl sm:text-xl text-center mb-6 text-[#007BAC]">
Just Call Tiara
</h2>
<div className="w-full mb-10 flex justify-center">
<Image
src="/tiara.png"
alt="Lokasi Servis Terdekat"
width={500}
height={500}
className="w-[600px] object-cover rounded"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-10">
<div className="text-start px-4 border-l border-gray-300">
<div className="mb-14 ml-3">
<div className="text-4xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 24 24"
>
<g fill="none">
<path
stroke="currentColor"
// stroke-linejoin="round"
// stroke-width="2"
d="M12 13v7.098c0 .399 0 .598-.129.67c-.129.071-.298-.035-.636-.246L4.94 16.588c-.46-.288-.69-.431-.815-.658C4 15.705 4 15.434 4 14.893V8m8 5L4 8m8 5l5.286-3.304c1.218-.761 1.827-1.142 1.827-1.696s-.609-.935-1.827-1.696L13.06 3.663c-.516-.323-.773-.484-1.06-.484s-.544.161-1.06.484L4 8"
/>
<path
fill="currentColor"
d="M19 12a1 1 0 1 0 2 0zm.875-3.93L19 8.553zM19 9.107V12h2V9.108zm.59-2.544l-3.06-1.912l-1.06 1.696l3.06 1.913zM21 9.109c0-.252.001-.51-.02-.733a2 2 0 0 0-.23-.79l-1.75.97c-.027-.05-.02-.073-.011.01c.01.106.011.254.011.543zm-2.47-.848c.246.154.37.233.454.298c.067.05.043.045.016-.004l1.75-.97a2 2 0 0 0-.549-.614c-.177-.136-.397-.272-.611-.405z"
/>
<circle
cx="17.5"
cy="16.5"
r="2.5"
stroke="currentColor"
// stroke-width="2"
/>
<path
stroke="currentColor"
// stroke-linecap="round"
// stroke-width="2"
d="m21 20l-1.5-1.5"
/>
<path
fill="currentColor"
d="M14.53 20.598a1 1 0 0 0-1.06-1.696zM11 20.375l-.53.848zm.937.444l-.063.998zm.126 0L12 19.82zm-.533-1.292l-3-1.875l-1.06 1.696l3 1.875zm1.94-.625l-.5.313l1.06 1.695l.5-.312zm-.5.313l-.5.312l1.06 1.696l.5-.312zm-2.5 2.008c.213.133.429.27.625.368c.214.108.47.206.779.226L12 19.82c.056.003.072.022-.005-.016a7 7 0 0 1-.465-.278zm2-1.696a7 7 0 0 1-.465.278c-.077.038-.061.02-.005.016l.126 1.996c.31-.02.565-.118.779-.226c.196-.099.412-.235.625-.368zm-.596 2.29q.126.008.252 0L12 19.82z"
/>
</g>
</svg>
</div>
<p className="font-semibold mb-1"> {">"}TERSEDIA DALAM STOK</p>
<p className="text-gray-600 text-sm">
Jelajahi pilihan hebat kami dari mobil Jaecoo
</p>
</div>
</div>
<div className="text-start px-4 border-l border-r border-gray-300">
<div className="mb-14 ml-3">
<div className="text-2xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z"
/>
</svg>
</div>
<p className="font-semibold mb-1">{">"}BERITAHU SAYA</p>
<p className="text-gray-600 text-sm">
Daftar untuk semua berita terbaru dari Jaecoo
</p>
</div>
</div>
<div className="text-start px-4 border-r border-gray-300">
<div className="mb-14">
<div className="text-2xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M256 42.667c117.821 0 213.334 95.513 213.334 213.333c0 117.821-95.513 213.334-213.334 213.334c-117.82 0-213.333-95.513-213.333-213.334C42.667 138.18 138.18 42.667 256 42.667M85.334 256c0 87.032 65.145 158.848 149.332 169.346V316.358c-21.87-7.73-38.283-27.01-41.913-50.51L85.636 245.762q-.301 5.081-.302 10.238m341.031-10.238l-107.118 20.086c-3.629 23.5-20.043 42.78-41.913 50.51v108.988C361.523 414.848 426.668 343.032 426.668 256q-.001-5.156-.302-10.238M256 85.334c-76.056 0-140.493 49.75-162.541 118.484l107.16 20.085C211.699 204.827 232.352 192 256 192c23.65 0 44.302 12.827 55.382 31.903l107.16-20.085C396.493 135.084 332.057 85.334 256 85.334"
/>
</svg>
</div>
<p className="font-semibold mb-1">{">"}PESAN TEST DRIVE</p>
<p className="text-gray-600 text-sm">
Atur test drive di jalan melalui Dealer terdekat kami
</p>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,86 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
const featuresJ8Awd = [
{
title: "Crystal Drive Mode Selector",
description:
"The stunning Crystal Drive Mode Selector offers seven distinct drive modes: City, Snow, Sand, Mud, Normal, ECO, and Sport. Its elegant design elevates cabin aesthetics while providing intuitive fingertip access to tailor the driving experience — from smooth city commutes to off-road adventures.",
image: "/in-j8awd2.png",
},
{
title: "Headrest Speaker",
description:
"Embedded within the premium 14-speaker Sony sound system, the headrest speakers deliver immersive, high-fidelity audio directly to occupants. Designed to enhance entertainment quality and ensure privacy during calls, this feature offers clear sound without disturbing others, blending innovation with comfort.",
image: "/in-j8awd3.png",
},
{
title: "Zero Gravity Seat",
description:
"Experience true zero-gravity relaxation with an adjustable 123° seat angle, an exclusive sleep headrest, and adjustable earpieces for an optimal fit. Double-layer noise-canceling acoustic glass effectively blocks out external noise, creating a peaceful retreat from the busy world.",
image: "/in-j8awd4.png",
},
];
export default function InteriorJ8Awd() {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 });
return (
<section className="py-10 px-4 sm:px-6 md:px-20 bg-white" ref={ref}>
<motion.h2
className="text-2xl mt-5 mb-8"
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
Interior
</motion.h2>
<motion.div
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]"
initial={{ opacity: 0, scale: 0.95 }}
animate={inView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.7 }}
>
<Image
src="/in-j8awd1.png"
alt="Interior Hero"
fill
className="object-cover"
sizes="100vw"
/>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 sm:gap-4 mt-5">
{featuresJ8Awd.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[3/2] overflow-hidden group"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,146 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
const featuresInt = [
{
title: "14.8 Screen with APPLE Carplay & Android Auto",
description:
"Stay connected and informed with a 14.8 display offering clear visuals and advanced functionality for a seamless driving experience.",
image: "/in-shs2.png",
},
{
title: "Horizontal Side by Side Cup Holder",
description:
"Keep your beverages secure and within reach with the stylish Horizontal Side-by-Side Cup Holder.",
image: "/in-shs3.png",
},
{
title: "EV/ HEV Button",
description:
"Effortlessly switch between power modes with the EV/HEV Button, designed for optimal driving efficiency.",
image: "/in-shs4.png",
},
{
title: "Wireless Charging",
description:
"Stay powered up on the go with Wireless Charging, ensuring your devices are always ready when you are.",
image: "/in-shs5.png",
},
];
const interior = [
{
title: "Dual Door Armrest",
description:
"A seamless blend of style and performance with the Avantgrade Fighter-Inspired Transmission Shifter.",
image: "/in-shs6.png",
},
{
title: "Ventilated Leather Seats",
description:
"Stay cool and comfortable with Ventilated Leather Seats, designed for luxury and relaxation.",
image: "/in-shs7.png",
},
{
title: "Sony 8-Speaker Audio system",
description:
"Immerse yourself in rich, high-quality sound with the Sony 8-speaker audio system, delivering an exceptional listening experience on every journey.",
image: "/in-shs8.png",
},
{
title: "Minimalist Door Latch Design",
description:
"Redefine sophistication with the Minimalist Door Latch Design, offering a seamless blend of style and utility.",
image: "/in-shs9.png",
},
];
export default function InteriorShs() {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 });
return (
<section className="py-10 px-4 sm:px-6 md:px-20 bg-white" ref={ref}>
<motion.h2
className="text-2xl mt-5 mb-8"
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
Interior
</motion.h2>
<motion.div
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]"
initial={{ opacity: 0, scale: 0.95 }}
animate={inView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.7 }}
>
<Image
src="/in-shs.png"
alt="Interior Hero"
fill
className="object-cover"
sizes="100vw"
/>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5">
{featuresInt.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5">
{interior.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,140 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useInView } from "react-intersection-observer";
const features = [
{
title: "14.8 Screen with APPLE Carplay & Android Auto",
description:
"Stay connected and informed with a 14.8 display offering clear visuals and advanced functionality for a seamless driving experience.",
image: "/interior-2.png",
},
{
title: "Windshield Heads-Up Display (W-HUD)",
description:
"Stay informed and focused on the road with the adjustable W-HUD, tailored to align perfectly with your unique sitting position for optimal driving comfort and safety.",
image: "/interior-3.png",
},
{
title: "540 degree HD video",
description:
"The 540-degree HD video system provides comprehensive coverage, ensuring nothing escapes your view.",
image: "/interior-4.png",
},
{
title: "Wireless Charging",
description:
"Stay powered up on the go with Wireless Charging, ensuring your devices are always ready when you are.",
image: "/interior-5.png",
},
];
const interior = [
{
title: "Aircraft-style Gear Shift",
description:
"A seamless blend of style and performance with the Avantgrade Fighter-Inspired Transmission Shifter.",
image: "/interior-6.png",
},
{
title: "Heated/Ventilated Seats",
description:
"Enjoy ultimate comfort with synthetic leather seats featuring heating and ventilation, designed for a luxurious and adaptable driving experience.",
image: "/interior-7.png",
},
{
title: "Sony 8-Speaker Audio system",
description:
"Immerse yourself in rich, high-quality sound with the Sony 8-speaker audio system, delivering an exceptional listening experience on every journey.",
image: "/interior-8.png",
},
];
export default function Interior() {
const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 });
return (
<section className="py-10 px-4 sm:px-6 md:px-20 bg-white" ref={ref}>
<motion.h2
className="text-2xl mt-5 mb-8"
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span>{" "}
Interior
</motion.h2>
<motion.div
className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]"
initial={{ opacity: 0, scale: 0.95 }}
animate={inView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.7 }}
>
<Image
src="/interior-1.png"
alt="Interior Hero"
fill
className="object-cover"
sizes="100vw"
/>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4 mt-5">
{features.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[4/3] overflow-hidden group"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 sm:gap-4 mt-5">
{interior.map((item, index) => (
<motion.div
key={index}
className="relative aspect-[2/1] overflow-hidden group"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 25vw"
/>
<div className="absolute bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<h3 className="text-sm sm:text-base font-bold text-white">
{item.title}
</h3>
<p className="text-xs sm:text-sm text-gray-300 mt-1">
{item.description}
</p>
</div>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,150 @@
"use client";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { motion } from "framer-motion";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { useState } from "react";
const items = [
{
image: "/new-car2.png",
title: "JAECOO J7 AWD",
description: "DELICATE OFF-ROAD SUV",
link: "/product/j7-awd",
},
{
image: "/new-car1.png",
title: "JAECOO J7 SHS",
description: "SUPER HYBRID SYSTEM = SUPER HEV + EV",
link: "/product/j7-shs",
},
{
image: "/new-car3.png",
title: "JAECOO J8 AWD",
description: "FIRST CLASS OFF-ROAD",
link: "/product/j8-awd",
},
];
export default function Items() {
const [open, setOpen] = useState(false);
return (
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<div className="flex flex-col items-center gap-10">
{items.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.7,
delay: index * 0.2,
ease: "easeOut",
}}
className="relative w-full min-h-[400px] sm:min-h-[500px] md:min-h-[600px] overflow-hidden"
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 768px"
priority
/>
<div className="absolute inset-0 z-10 flex flex-col justify-between">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.25 + 0.3, duration: 0.5 }}
className="mt-3 ml-3 font-semibold text-white text-sm sm:text-xl px-4 py-2 rounded-lg max-w-[80%]"
>
{item.description}
</motion.div>
<div className="flex flex-col items-center pb-8 bg-gradient-to-t to-transparent">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.25 + 0.4, duration: 0.6 }}
className="text-2xl sm:text-3xl md:text-2xl font-semibold text-white mb-4 text-center"
>
{item.title}
</motion.h1>
<motion.div
className="flex items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.25 + 0.6, duration: 0.6 }}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:h-[40px] rounded-full hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-2xl text-center mb-4">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* 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={item?.link}>
<Button
variant="outline"
className="rounded-full border-white text-black px-6 py-2 hover:text-black hover:bg-amber-50 hover:border-white hover:cursor-pointer"
>
EXPLORE
</Button>
</Link>
</motion.div>
</div>
</div>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,171 @@
"use client";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useState } from "react";
export default function FormJaecoo() {
const [consent, setConsent] = useState(false);
return (
<section className="px-4 py-12 md:px-20 bg-white">
<div className="max-w-full mx-auto">
<h2 className="text-3xl font-bold mb-8 text-black">
Get In Touch With Us
</h2>
<form className="space-y-6">
<div>
<Label htmlFor="name" className="mb-2">
Name
</Label>
<Input
id="name"
placeholder="Type your name here…"
className="w-full h-12 text-base"
/>
</div>
<div>
<Label htmlFor="email" className="mb-2">
Email
</Label>
<Input
id="email"
type="email"
placeholder="Rachel@domain.com"
className="w-full h-12 text-base"
/>
</div>
<div>
<Label htmlFor="phone" className="mb-2">
Mobile Number
</Label>
<Input
id="phone"
type="tel"
placeholder="+62 8xxxx"
className="w-full h-12 text-base"
/>
</div>
<div>
<Label htmlFor="city" className="mb-2 block">
City
</Label>
<Select>
<SelectTrigger className="w-full h-12 text-base">
<SelectValue placeholder="Select city" />
</SelectTrigger>
<SelectContent>
<SelectItem value="jakarta">Jakarta</SelectItem>
<SelectItem value="bandung">Bandung</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="categories" className="mb-2">
Categories
</Label>
<Select>
<SelectTrigger className="w-full h-12 text-base">
<SelectValue placeholder="Select categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ev">Electric Vehicle</SelectItem>
<SelectItem value="suv">SUV</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="series" className="mb-2">
Product Series
</Label>
<Select>
<SelectTrigger className="w-full h-12 text-base">
<SelectValue placeholder="Select product series" />
</SelectTrigger>
<SelectContent>
<SelectItem value="e100">E100</SelectItem>
<SelectItem value="e200">E200</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="showroom" className="mb-2">
Showroom
</Label>
<Select>
<SelectTrigger className="w-full h-12 text-base">
<SelectValue placeholder="Select showroom" />
</SelectTrigger>
<SelectContent>
<SelectItem value="jakarta">Jakarta</SelectItem>
<SelectItem value="bandung">Bandung</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="subject" className="mb-2">
Subject
</Label>
<Input
id="subject"
placeholder="Type your subject here…"
className="w-full h-12 text-base"
/>
</div>
<div>
<Label htmlFor="message" className="mb-2">
Message
</Label>
<Textarea
id="message"
placeholder="Type your query here…"
className="w-full h-32 resize-none text-base"
/>
</div>
<div className="flex items-start gap-2">
<Checkbox
id="consent"
checked={consent}
onCheckedChange={() => setConsent(!consent)}
/>
<label htmlFor="consent" className="text-sm text-gray-700">
By providing your information, you consent to the collection, use
and disclosure of your personal data by PT. Inchcape Indomobil
Energi Baru and our trusted third parties (our related
corporations and affiliates, selected partners, service providers,
agents and other Inchcape Indomobil Energi Baru companies) in
accordance with the purposes set out in our privacy policy.
</label>
</div>
<Button
size="lg"
type="submit"
className="bg-[#008bcf] hover:bg-[#0072a8] w-[250px] h-[50px] rounded-full text-white"
>
Submit
</Button>
</form>
</div>
</section>
);
}

View File

@ -0,0 +1,118 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { ChevronLeft, ChevronRight } from "lucide-react";
const locations = [
{
name: "Jaecoo 1S Kelapa Gading",
address:
"Jl. Boulevard Raya Blok LA 6 No. 34-35, Kelapa Gading, Jakarta Utara 14240",
region: "Kelapa Gading, Jakarta",
image: "/loc1.png",
},
{
name: "Jaecoo Cihampelas Bandung",
address: "Jl. Cihampelas No. 264-268, Bandung, Jawa Barat 40131",
region: "Cihampelas, Bandung",
image: "/loc2.png",
},
{
name: "Jaecoo 2S Kelapa Gading",
address: "Jl. Pegangsaan Dua No.17 B, Kelapa Gading, Jakarta Utara 14250",
region: "Kelapa Gading, Jakarta",
image: "/loc3.png",
},
];
export default function Location() {
const [currentPage, setCurrentPage] = useState(1);
const perPage = 3;
const totalPages = Math.ceil(locations.length / perPage);
const paginated = locations.slice(
(currentPage - 1) * perPage,
currentPage * perPage
);
const handlePrev = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const handleNext = () => {
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
};
return (
<section className="max-w-7xl mx-auto py-16 px-6 md:px-12 bg-white">
<div className="flex flex-col md:flex-row mx-2 items-center justify-between mb-10">
<h2 className="text-2xl md:text-6xl font-bold text-gray-900 text-center mb-2 md:md-0">
Cari Store Lain
</h2>
<div className="flex flex-row gap-3">
<div className=" max-w-xl">
<input
type="text"
placeholder="Cari lokasi..."
className="w-full py-3 pl-5 pr-20 border border-gray-300 rounded-full text-gray-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>{" "}
<button className=" bg-[#00696e] text-white px-6 py-2 rounded-full font-medium">
Cari
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{paginated.map((store, index) => (
<div key={index} className="bg-white shadow-md overflow-hidden">
<div className="relative w-full h-64">
<Image
src={store.image}
alt={store.name}
fill
className="object-cover"
/>
</div>
<div className="p-4">
<p className="font-semibold text-black">
{store.name}, {store.address}
</p>
<p className="text-gray-500 mt-1">{store.region}</p>
</div>
</div>
))}
</div>
<div className="flex items-center justify-center gap-4 mt-10">
<button
onClick={handlePrev}
disabled={currentPage === 1}
className="p-2 rounded-full bg-gray-100 hover:bg-gray-200 disabled:opacity-50"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(totalPages)].map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentPage(idx + 1)}
className={`w-8 h-8 rounded-full text-sm font-medium ${
currentPage === idx + 1
? "bg-[#00696e] text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{idx + 1}
</button>
))}
<button
onClick={handleNext}
disabled={currentPage === totalPages}
className="p-2 rounded-full bg-gray-100 hover:bg-gray-200 disabled:opacity-50"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</section>
);
}

View File

@ -0,0 +1,540 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ChevronDown, Lock, Menu, X } from "lucide-react";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { AnimatePresence, motion } from "framer-motion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
export default function Navbar() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [showProdukMenu, setShowProdukMenu] = useState(false);
const [showPriceMenu, setShowPriceMenu] = useState(false);
const [showServiceMenu, setShowServiceMenu] = useState(false);
const [showAboutMenu, setShowAboutMenu] = useState(false);
const [showConsumerMenu, setShowConsumerMenu] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const isActive = (path: string) =>
pathname === path || pathname.startsWith(path + "/");
useEffect(() => {
const handleClickOutside = () => {
setShowProdukMenu(false);
};
window.addEventListener("click", handleClickOutside);
return () => window.removeEventListener("click", handleClickOutside);
}, []);
const produkList = [
{
name: "JAECOO J7 AWD",
img: "/j7awd.png",
link: "/product/j7-awd",
},
{
name: "JAECOO J7 SHS",
img: "/j7shs.png",
link: "/product/j7-shs",
},
{
name: "JAECOO J8 AWD",
img: "/j8awd.png",
link: "/product/j8-awd",
},
];
const priceList = [
{
name: "INFORMASI HARGA",
link: "/price/price-information",
},
{
name: "PROMO",
link: "/price/promo",
},
];
const serviceList = [
{
name: "PROGRAM SERVICE",
link: "/service/program-service",
},
{
name: "AFTER SALES",
link: "/service/after-sales",
},
];
const aboutList = [
{
name: "PROFILE",
link: "/about/profile",
},
{
name: "SOCIAL MEDIA",
link: "/about/sosmed",
},
{
name: "GALERY",
link: "/about/galery",
},
];
const consumerList = [
{
name: "AFTER SALES",
link: "/customer-service/after-sales",
},
{
name: "SALES",
link: "/customer-service/sales",
},
];
return (
<nav className="relative w-full flex items-center justify-between py-4 px-6 sm:px-10 bg-white z-50">
<div className="flex items-center gap-4">
<Link href="/" className="flex items-center space-x-2">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</Link>
</div>
<button
className="sm:hidden absolute right-6 text-[#1F3D4A]"
onClick={() => setIsMobileMenuOpen((prev) => !prev)}
>
{isMobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
<ul className="hidden sm:flex mx-auto items-center gap-4 sm:gap-6 text-sm font-medium">
<li>
<Link href="/">
<Button
variant="ghost"
className={`hover:cursor-pointer rounded-full font-bold px-5 ${
isActive("/") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
HOMEPAGE
</Button>
</Link>
</li>
<li className="">
<Button
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setShowProdukMenu((prev) => !prev);
}}
className={`flex items-center gap-1 font-bold text-sm focus:outline-none hover:cursor-pointer rounded-full px-5 py-2 ${
isActive("/product") || isActive("/produk")
? "bg-[#C2D8E2] text-[#1F3D4A]"
: ""
}`}
>
PRODUCTS <ChevronDown className="w-4 h-4" />
</Button>
<AnimatePresence>
{showProdukMenu && (
<motion.div
key="produk-dropdown"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
className="absolute left-0 right-0 top-[calc(100%+1rem)] z-50 bg-white shadow-xl px-6 sm:px-10 py-6 rounded-xl w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="max-w-screen-xl mx-auto w-full flex flex-col sm:flex-row items-center sm:items-start justify-between gap-y-10 sm:gap-y-0 sm:gap-x-6 text-center sm:text-left">
{produkList.map((car, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 30 }}
transition={{ delay: 0.2 + i * 0.2, duration: 0.5 }}
className="flex flex-col items-center text-center w-full sm:w-auto"
>
<Image
src={car.img}
alt={car.name}
width={250}
height={150}
className="object-contain"
/>
<p className="font-bold mt-4 text-center">{car.name}</p>
<div className="flex flex-col sm:flex-row gap-2 mt-2 items-center">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-[#1F6779] text-white h-[30px] md:w-[200px] md:h-[40px] rounded-full hover:cursor-pointer">
TEST DRIVE
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1400px] h-[600px]">
<div className="flex items-center gap-4">
<Image
src="/masjaecoonav.png"
alt="MAS JAECOO Logo"
width={300}
height={30}
className=" object-fill"
/>
</div>
<DialogHeader>
<DialogTitle className="text-4xl text-center mb-4 font-bold">
FORM TEST DRIVE
</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-10">
<Input placeholder="Nama" />
<Input placeholder="Email" />
<Input placeholder="Mobile Number" />
<Input placeholder="Location" />
</div>
<div className="mt-3 px-10">
<Textarea placeholder="Full Message" rows={4} />
</div>
<div className="mt-6 text-left ml-10">
<Button
onClick={() => setOpen(false)}
className="bg-[#1F6779] text-white rounded-full"
>
SEND INQUIRY
</Button>
</div>
</DialogContent>
</Dialog>
<Link href={car.link} className="w-[200px]">
<Button
variant="outline"
className="rounded-full px-4 w-full hover:cursor-pointer hover:bg-amber-50"
>
EXPLORE
</Button>
</Link>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
<li className="relative">
<Button
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setShowPriceMenu((prev) => !prev);
}}
className={`flex items-center gap-1 font-bold text-sm focus:outline-none hover:cursor-pointer rounded-full px-5 py-2 ${
isActive("/price") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
HARGA <ChevronDown className="w-4 h-4" />
</Button>
<AnimatePresence>
{showPriceMenu && (
<motion.div
key="harga-dropdown"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
className="absolute top-full mt-2 left-0 z-50 border-t-4 border-[#1F6779] bg-white shadow-xl py-4 w-[200px]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-2 px-4">
{priceList.map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 * i }}
>
<Link
href={item.link}
className="block w-full text-sm text-left px-3 py-2 rounded-md hover:bg-gray-100 hover:text-[#1F6779] font-medium"
>
{item.name}
</Link>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
<li className="relative">
<Button
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setShowServiceMenu((prev) => !prev);
}}
className={`flex items-center gap-1 font-bold text-sm focus:outline-none hover:cursor-pointer rounded-full px-5 py-2 ${
isActive("/service") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
SERVICES <ChevronDown className="w-4 h-4" />
</Button>
<AnimatePresence>
{showServiceMenu && (
<motion.div
key="harga-dropdown"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
className="absolute top-full mt-2 left-0 z-50 border-t-4 border-[#1F6779] bg-white shadow-xl py-4 w-[200px]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-2 px-4">
{serviceList.map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 * i }}
>
<Link
href={item.link}
className="block w-full text-sm text-left px-3 py-2 rounded-md hover:bg-gray-100 hover:text-[#1F6779] font-medium"
>
{item.name}
</Link>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
<li className="relative">
<Button
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setShowAboutMenu((prev) => !prev);
}}
className={`flex items-center gap-1 font-bold text-sm focus:outline-none hover:cursor-pointer rounded-full px-5 py-2 ${
isActive("/about") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
TENTANG DEALER <ChevronDown className="w-4 h-4" />
</Button>
<AnimatePresence>
{showAboutMenu && (
<motion.div
key="harga-dropdown"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
className="absolute top-full mt-2 left-0 z-50 border-t-4 border-[#1F6779] bg-white shadow-xl py-4 w-[200px]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-2 px-4">
{aboutList.map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 * i }}
>
<Link
href={item.link}
className="block w-full text-sm text-left px-3 py-2 rounded-md hover:bg-gray-100 hover:text-[#1F6779] font-medium"
>
{item.name}
</Link>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
<li className="relative">
<Button
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setShowConsumerMenu((prev) => !prev);
}}
className={`flex items-center gap-1 font-bold text-sm focus:outline-none hover:cursor-pointer rounded-full px-5 py-2 ${
isActive("/customer-service") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
LAYANAN KONSUMEN <ChevronDown className="w-4 h-4" />
</Button>
<AnimatePresence>
{showConsumerMenu && (
<motion.div
key="harga-dropdown"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
className="absolute top-full mt-2 left-0 z-50 border-t-4 border-[#1F6779] bg-white shadow-xl py-4 w-[200px]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-2 px-4">
{consumerList.map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 * i }}
>
<Link
href={item.link}
className="block w-full text-sm text-left px-3 py-2 rounded-md hover:bg-gray-100 hover:text-[#1F6779] font-medium"
>
{item.name}
</Link>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</li>
</ul>
<div className="hidden sm:block">
<Link href="/auth">
<Button className="bg-[#1F6779]">
<Lock className="w-3 h-3 mr-1" />
Login
</Button>
</Link>
</div>
{isMobileMenuOpen && (
<div className="absolute top-full left-0 right-0 bg-white px-6 py-4 shadow-md flex flex-col gap-4 text-sm font-medium sm:hidden z-40">
<Link href="/" onClick={() => setIsMobileMenuOpen(false)}>
<Button
variant="ghost"
className={`w-full justify-start ${
isActive("/") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
BERANDA
</Button>
</Link>
<Link href="/product" onClick={() => setIsMobileMenuOpen(false)}>
<Button
variant="ghost"
className={`flex items-center gap-1 font-bold text-sm focus:outline-none rounded-full px-5 py-2 ${
isActive("/product") || isActive("/produk")
? "bg-[#C2D8E2] text-[#1F3D4A]"
: ""
}`}
>
PRODUK
</Button>
</Link>
<Link
href="/price/price-information"
onClick={() => setIsMobileMenuOpen(false)}
>
<Button
variant="ghost"
className={`w-full justify-start ${
isActive("/price/information")
? "bg-[#C2D8E2] text-[#1F3D4A]"
: ""
}`}
>
HARGA
</Button>
</Link>
<Link
href="/service/program-service"
onClick={() => setIsMobileMenuOpen(false)}
>
<Button
variant="ghost"
className={`w-full justify-start ${
isActive("/service/program-service")
? "bg-[#C2D8E2] text-[#1F3D4A]"
: ""
}`}
>
SERVICES
</Button>
</Link>
<Link href="/about/galery" onClick={() => setIsMobileMenuOpen(false)}>
<Button
variant="ghost"
className={`w-full justify-start ${
isActive("/about/galery") ? "bg-[#C2D8E2] text-[#1F3D4A]" : ""
}`}
>
TENTANG DEALER JAECOO
</Button>
</Link>
<Link
href="/customer-service/after-sales"
onClick={() => setIsMobileMenuOpen(false)}
>
<Button
variant="ghost"
className={`w-full justify-start ${
isActive("/customer-service/after-sales")
? "bg-[#C2D8E2] text-[#1F3D4A]"
: ""
}`}
>
LAYANAN KONSUMEN
</Button>
</Link>
<Link href="/auth" onClick={() => setIsMobileMenuOpen(false)}>
<Button className="bg-[#1F6779] w-full justify-start">
<Lock className="w-3 h-3 mr-1" />
Login
</Button>
</Link>
</div>
)}
</nav>
);
}

View File

@ -0,0 +1,109 @@
import Image from "next/image";
export default function NearestLocation() {
return (
<section className="max-w-[1400px] mx-auto bg-white py-16 px-4 sm:px-6 lg:px-10">
<h2 className="text-2xl sm:text-3xl font-bold text-center mb-6">
Lokasi Servis Terdekat
</h2>
<div className="w-full mb-10">
<Image
src="/map-service.png"
alt="Lokasi Servis Terdekat"
width={1200}
height={600}
className="w-full object-cover rounded"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 pt-10">
<div className="text-start px-4 border-l border-gray-300">
<div className="text-4xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 24 24"
>
<g fill="none">
<path
stroke="currentColor"
// stroke-linejoin="round"
// stroke-width="2"
d="M12 13v7.098c0 .399 0 .598-.129.67c-.129.071-.298-.035-.636-.246L4.94 16.588c-.46-.288-.69-.431-.815-.658C4 15.705 4 15.434 4 14.893V8m8 5L4 8m8 5l5.286-3.304c1.218-.761 1.827-1.142 1.827-1.696s-.609-.935-1.827-1.696L13.06 3.663c-.516-.323-.773-.484-1.06-.484s-.544.161-1.06.484L4 8"
/>
<path
fill="currentColor"
d="M19 12a1 1 0 1 0 2 0zm.875-3.93L19 8.553zM19 9.107V12h2V9.108zm.59-2.544l-3.06-1.912l-1.06 1.696l3.06 1.913zM21 9.109c0-.252.001-.51-.02-.733a2 2 0 0 0-.23-.79l-1.75.97c-.027-.05-.02-.073-.011.01c.01.106.011.254.011.543zm-2.47-.848c.246.154.37.233.454.298c.067.05.043.045.016-.004l1.75-.97a2 2 0 0 0-.549-.614c-.177-.136-.397-.272-.611-.405z"
/>
<circle
cx="17.5"
cy="16.5"
r="2.5"
stroke="currentColor"
// stroke-width="2"
/>
<path
stroke="currentColor"
// stroke-linecap="round"
// stroke-width="2"
d="m21 20l-1.5-1.5"
/>
<path
fill="currentColor"
d="M14.53 20.598a1 1 0 0 0-1.06-1.696zM11 20.375l-.53.848zm.937.444l-.063.998zm.126 0L12 19.82zm-.533-1.292l-3-1.875l-1.06 1.696l3 1.875zm1.94-.625l-.5.313l1.06 1.695l.5-.312zm-.5.313l-.5.312l1.06 1.696l.5-.312zm-2.5 2.008c.213.133.429.27.625.368c.214.108.47.206.779.226L12 19.82c.056.003.072.022-.005-.016a7 7 0 0 1-.465-.278zm2-1.696a7 7 0 0 1-.465.278c-.077.038-.061.02-.005.016l.126 1.996c.31-.02.565-.118.779-.226c.196-.099.412-.235.625-.368zm-.596 2.29q.126.008.252 0L12 19.82z"
/>
</g>
</svg>
</div>
<p className="font-semibold mb-1"> {">"}TERSEDIA DALAM STOK</p>
<p className="text-gray-600 text-sm">
Jelajahi pilihan hebat kami dari mobil Jaecoo
</p>
</div>
<div className="text-start px-4 border-l border-r border-gray-300">
<div className="text-2xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z"
/>
</svg>
</div>
<p className="font-semibold mb-1">{">"}BERITAHU SAYA</p>
<p className="text-gray-600 text-sm">
Daftar untuk semua berita terbaru dari Jaecoo
</p>
</div>
<div className="text-start px-4 border-r border-gray-300">
<div className="text-2xl mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M256 42.667c117.821 0 213.334 95.513 213.334 213.333c0 117.821-95.513 213.334-213.334 213.334c-117.82 0-213.333-95.513-213.333-213.334C42.667 138.18 138.18 42.667 256 42.667M85.334 256c0 87.032 65.145 158.848 149.332 169.346V316.358c-21.87-7.73-38.283-27.01-41.913-50.51L85.636 245.762q-.301 5.081-.302 10.238m341.031-10.238l-107.118 20.086c-3.629 23.5-20.043 42.78-41.913 50.51v108.988C361.523 414.848 426.668 343.032 426.668 256q-.001-5.156-.302-10.238M256 85.334c-76.056 0-140.493 49.75-162.541 118.484l107.16 20.085C211.699 204.827 232.352 192 256 192c23.65 0 44.302 12.827 55.382 31.903l107.16-20.085C396.493 135.084 332.057 85.334 256 85.334"
/>
</svg>
</div>
<p className="font-semibold mb-1">{">"}PESAN TEST DRIVE</p>
<p className="text-gray-600 text-sm">
Atur test drive di jalan melalui Dealer terdekat kami
</p>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,120 @@
import { motion } from "framer-motion";
import { useState, Dispatch, SetStateAction } from "react";
export type OptionProps = {
Icon: any;
title: string;
selected?: string;
setSelected?: Dispatch<SetStateAction<string>>;
open: boolean;
notifs?: number;
active?: boolean;
};
const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: OptionProps) => {
const [hovered, setHovered] = useState(false);
const isActive = active ?? selected === title;
return (
<motion.button
layout
onClick={() => setSelected?.(title)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`relative flex h-12 w-full px-3 items-center rounded-xl transition-all duration-200 cursor-pointer group ${
isActive
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{/* Active indicator */}
{isActive && (
<motion.div
layoutId="activeIndicator"
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-sm"
initial={{ opacity: 0, scaleY: 0 }}
animate={{ opacity: 1, scaleY: 1 }}
transition={{ duration: 0.2 }}
/>
)}
<motion.div
layout
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div className={`text-lg transition-all duration-200 ${
isActive
? "text-white"
: "text-slate-500 group-hover:text-slate-700"
}`}>
<Icon />
</div>
</motion.div>
{open && (
<motion.span
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
isActive ? "text-white" : "text-slate-700"
}`}
>
{title}
</motion.span>
)}
{/* Tooltip for collapsed state */}
{!open && hovered && (
<motion.div
initial={{ opacity: 0, x: 8, scale: 0.8 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 8, scale: 0.8 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="absolute left-full ml-3 whitespace-nowrap rounded-lg bg-slate-800 px-3 py-2 text-sm text-white shadow-xl z-50"
>
<div className="relative">
{title}
{/* Tooltip arrow */}
<div className="absolute -left-1 top-1/2 -translate-y-1/2 w-2 h-2 bg-slate-800 rotate-45"></div>
</div>
</motion.div>
)}
{/* Notification badge */}
{notifs && open && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className={`absolute right-3 top-1/2 -translate-y-1/2 size-5 rounded-full text-xs font-semibold flex items-center justify-center ${
isActive
? "bg-white text-emerald-500"
: "bg-red-500 text-white"
}`}
>
{notifs}
</motion.span>
)}
{/* Hover effect overlay */}
{hovered && !isActive && (
<motion.div
layoutId="hoverOverlay"
className="absolute inset-0 bg-gradient-to-r from-slate-100/50 to-slate-200/50 rounded-xl"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button>
);
};
export default Option;

View File

@ -0,0 +1,41 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
export default function HeaderProgramSales() {
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<h2 className="text-4xl font-bold mb-1">Program Services</h2>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/promo.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden mt-5">
<Image
src="/promo.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,30 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
export default function HeaderPromo() {
return (
<>
<section className="py-10 px-4 sm:px-6 md:px-10 bg-white">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col items-center gap-6"
>
<div className="relative w-full max-w-[1400px] mx-auto h-[300px] sm:h-[400px] md:h-[640px] overflow-hidden">
<Image
src="/promo.png"
alt="about-header"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 640px"
priority
/>
</div>
</motion.div>
</section>
</>
);
}

View File

@ -0,0 +1,589 @@
"use client";
import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import Link from "next/link";
import DashboardContainer from "../main/dashboard/dashboard-container";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { useTheme } from "../layout/theme-context";
import Option from "./option";
interface RetractingSidebarProps {
sidebarData: boolean;
updateSidebarData: (newData: boolean) => void;
}
const sidebarSections = [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
title: "Content Management",
items: [
{
title: "Articles",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/article",
},
{
title: "Categories",
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
link: "/admin/master-category",
},
// {
// title: "Majalah",
// icon: () => <Icon icon="emojione-monotone:newspaper" className="text-lg" />,
// link: "/admin/magazine",
// },
{
title: "Advertisements",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/advertise",
},
// {
// title: "Komentar",
// icon: () => <Icon icon="material-symbols:comment-outline-rounded" className="text-lg" />,
// link: "/admin/komentar",
// },
],
},
{
title: "System",
items: [
{
title: "Static Pages",
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
link: "/admin/static-page",
},
{
title: "User Management",
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
link: "/admin/master-user",
},
],
},
];
export const RetractingSidebar = ({
sidebarData,
updateSidebarData,
}: RetractingSidebarProps) => {
const pathname = usePathname();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{/* DESKTOP SIDEBAR */}
<AnimatePresence mode="wait">
<motion.nav
key="desktop-sidebar"
layout
className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-r border-slate-200/60 dark:border-slate-700/60 shadow-lg backdrop-blur-sm flex-col justify-between"
style={{
width: sidebarData ? "280px" : "80px",
}}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<SidebarContent
open={sidebarData}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.nav>
</AnimatePresence>
{/* Desktop Toggle Button - appears when sidebar is collapsed */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="desktop-toggle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="hidden md:flex fixed top-4 left-20 z-40 p-3 bg-white rounded-xl shadow-lg border border-slate-200/60 hover:shadow-xl transition-all duration-200 hover:bg-slate-50"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
<AnimatePresence>
{!sidebarData && (
<motion.button
key="mobile-toggle"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
className="md:hidden fixed top-4 left-4 z-50 p-3 bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200/60 dark:border-slate-700/60 hover:shadow-xl transition-all duration-200"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-6 h-6 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
<AnimatePresence>
{sidebarData && (
<motion.div
key="mobile-sidebar"
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "tween", duration: 0.3 }}
className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 p-4 flex flex-col md:hidden shadow-2xl backdrop-blur-sm"
>
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
</button> */}
<SidebarContent
open={true}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
};
const SidebarContent = ({
open,
pathname,
updateSidebarData,
}: {
open: boolean;
pathname: string;
updateSidebarData: (newData: boolean) => void;
}) => {
const { theme, toggleTheme } = useTheme();
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col space-y-6">
<div className="flex items-center justify-between px-4 py-6">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
<img
src="/masjaecoo.png"
className="w-28 h-10 bg-black p-1 dark:bg-transparent"
/>
<div className="absolute opacity-20"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col"
>
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-black dark:text-white">
Jaecoo
</span>
<span className="text-xs text-slate-500">Admin Panel</span>
</motion.div>
)}
</Link>
{open && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 group"
onClick={() => updateSidebarData(false)}
>
<Icon
icon="heroicons:chevron-left"
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
/>
</motion.button>
)}
</div>
<div className="space-y-3 px-3 pb-6">
{sidebarSections.map((section, sectionIndex) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + sectionIndex * 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 + sectionIndex * 0.1 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-2">
{section.items.map((item, itemIndex) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
</div>
</motion.div>
))}
</div>
</div>
</div>
<div className="flex-shrink-0 space-y-1 border-t border-slate-200/60 dark:border-slate-700/60 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm">
<div className="px-3 pt-1">
<motion.button
onClick={toggleTheme}
className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
open ? "px-3" : "justify-center"
} ${
theme === "dark"
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700/50"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div
className={`text-lg transition-all duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-500 group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200"
}`}
>
{theme === "dark" ? (
<Icon icon="solar:sun-bold" className="text-lg" />
) : (
<Icon icon="solar:moon-bold" className="text-lg" />
)}
</div>
</motion.div>
{open && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-700 dark:text-slate-300"
}`}
>
{theme === "dark" ? "Light Mode" : "Dark Mode"}
</motion.span>
)}
</motion.button>
</div>
<div className="px-3">
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="px-3 py-3 border-t border-slate-200/60"
>
<div
className={`${
open
? "flex items-center space-x-3"
: "flex items-center justify-center"
} p-3 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100/50 hover:from-slate-100 hover:to-slate-200/50 transition-all duration-200 cursor-pointer group`}
>
<div className="relative">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
A
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex-1 min-w-0"
>
<p className="text-sm font-medium text-slate-800 truncate">
admin-mabes
</p>
<Link href="/auth">
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
Sign out
</p>
</Link>
</motion.div>
)}
</div>
</motion.div>
{/* Expand Button for Collapsed State */}
{/* {!open && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
className="px-3 pt-2"
>
<button
onClick={() => updateSidebarData(true)}
className="w-full p-3 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg transition-all duration-200 hover:shadow-xl group"
>
<div className="flex items-center justify-center">
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 group-hover:scale-110 transition-transform duration-200"
/>
</div>
</button>
</motion.div>
)} */}
</div>
</div>
);
};
const Sidebar = () => {
const [open, setOpen] = useState(true);
const pathname = usePathname();
return (
<motion.nav
layout
className="sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-1 hidden md:flex flex-col justify-between"
style={{
width: open ? "120px" : "90px",
}}
>
<div>
{!open && (
<div className="w-full flex justify-center items-center">
<button
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m10 17l5-5m0 0l-5-5"
/>
</svg>
</button>
</div>
)}
<div
className={`flex ${
open ? "justify-between" : "justify-center"
} w-full items-center px-2`}
>
<Link href="/" className="flex flex-row items-center gap-3 font-bold">
<img src="/assets/icon/Logo.png" className="w-20" />
</Link>
{open && (
<button
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m14 7l-5 5m0 0l5 5"
/>
</svg>
</button>
)}
</div>
<div className="space-y-1">
{sidebarSections.map((section) => (
<div key={section.title}>
<p className="font-bold text-[14px] py-2">{section.title}</p>
{section.items.map((item) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
</div>
))}
</div>
</div>
{/* BAGIAN BAWAH */}
<div className="space-y-1">
<Option
Icon={() => <Icon icon="solar:moon-bold" className="text-lg" />}
title="Theme"
active={false}
open={open}
/>
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>{" "}
<div className="flex flex-row gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="34"
height="34"
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="6" r="4" />
<path d="M20 17.5c0 2.485 0 4.5-8 4.5s-8-2.015-8-4.5S7.582 13 12 13s8 2.015 8 4.5Z" />
</g>
</svg>
<div className="flex flex-col gap-0.5 text-xs">
<p>admin-mabes</p>
<p className="underline">Logout</p>
</div>
</div>
</div>
</motion.nav>
);
};
export default Sidebar;
const TitleSection = ({ open }: { open: boolean }) => {
return (
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-slate-100">
<div className="flex items-center">
<motion.div
layout
initial={{ opacity: 0, y: 12, scale: 0.5 }}
animate={
open
? { opacity: 1, y: 0, scale: 1 }
: { opacity: 1, y: 0, scale: 0.5 }
}
transition={{ delay: 0.125 }}
>
<Image
src="/assets/icon/Logo.png"
alt="logo"
width={1920}
height={1080}
className="w-full h-fit"
/>
</motion.div>
</div>
{/* {open && <FiChevronDown className="mr-2" />} */}
</div>
);
};
const ToggleClose = ({
open,
setOpen,
}: {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}) => {
return (
<motion.button layout onClick={() => setOpen((pv) => !pv)}>
<div className="flex justify-center items-center pt-2">
<motion.div layout className="grid size-10 text-lg">
{/* <FiChevronsRight
className={`transition-transform ${open && "rotate-180"}`}
/> */}
</motion.div>
{/* {open && (
<motion.span layout initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.125 }} className="text-xs font-medium">
Hide
</motion.span>
)} */}
</div>
</motion.button>
);
};
const ExampleContent = () => (
<div>
<DashboardContainer />
</div>
);

View File

@ -0,0 +1,79 @@
"use client";
import Image from "next/image";
import { easeOut, motion } from "framer-motion";
const services = [
{ image: "/s1.png", title: "ELECTRONIC VEHICLE HEALTH CHECK" },
{ image: "/s2.png", title: "REQUEST A SERVICE" },
{ image: "/s3.png", title: "SERVICE PLANS" },
{ image: "/s4.png", title: "BODY AND PAINT" },
{ image: "/s5.png", title: "GENUINE PARTS" },
{ image: "/s6.png", title: "JAECOO REPAIRS" },
];
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.15,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: easeOut },
},
};
export default function Service() {
return (
<section className="py-16 px-4 sm:px-6 lg:px-10 bg-white">
<div className="w-full mx-auto text-start">
<motion.h2
className="text-2xl sm:text-3xl font-bold mb-2"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
viewport={{ once: true }}
>
Performa Hebat, Layanan Terjamin
</motion.h2>
<motion.p
className="text-gray-700 mb-10 w-full md:w-6/12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.1 }}
viewport={{ once: true }}
>
Servis resmi Jaecoo untuk kendaraan Anda dikerjakan oleh teknisi
tersertifikasi dengan suku cadang asli dan sistem booking online.
</motion.p>
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 justify-items-center"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
>
{services.map((service, index) => (
<motion.div key={index} variants={itemVariants} className="w-full">
<Image
src={service.image}
alt={service.title}
width={413}
height={170}
className="w-full object-contain"
/>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@ -0,0 +1,155 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
const tabs = ["INSTAGRAM", "TIKTOK", "FACEBOOK", "YOUTUBE"];
const instagramPosts = ["/ig1-new.png", "/ig2-new.png", "/ig3-new.png"];
const tiktokPosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
const youtubePosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
const facebookPosts = ["/tk1.png", "/tk2.png", "/tk3.png"];
export default function SosmedSection() {
const [activeTab, setActiveTab] = useState("INSTAGRAM");
return (
<section className="px-4 py-16 max-w-[1400px] mx-auto">
<h2 className="text-3xl font-bold mb-6 text-center">Sosial Media Kami</h2>
<div className="flex flex-wrap gap-4 items-center justify-center mb-8">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`text-sm font-medium px-4 py-2 rounded-full ${
activeTab === tab
? "bg-[#BCD4DF] text-sky-700"
: "text-[gray-700] hover:bg-gray-100"
}`}
>
{tab}
</button>
))}
</div>
{activeTab === "INSTAGRAM" && (
<>
<div className="flex flex-wrap justify-center items-center gap-4">
{instagramPosts.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={`Instagram post ${i + 1}`}
fill
className="w-full h-full object-cover"
/>
</div>
))}
</div>
<div className="flex justify-center mt-10">
<Link href={"https://www.instagram.com/jaecoo_cihampelasbdg"}>
<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 === "TIKTOK" && (
<>
<div className="flex flex-wrap justify-center items-center gap-4">
{tiktokPosts.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={`Tiktok post ${i + 1}`}
fill
className="w-full h-full object-cover"
/>
</div>
))}
</div>
<div className="flex justify-center mt-10">
<Link href={"https://www.tiktok.com/@jaecoo.cihampelasbdg"}>
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
Lihat Selengkapnya
<ArrowRight size={35} />
</button>
</Link>
</div>
</>
)}
{activeTab === "FACEBOOK" && (
<>
<div className="flex flex-wrap justify-center items-center gap-4">
{facebookPosts.map((img, i) => (
<div
key={i}
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
>
<Image
src={img}
alt={`Facebook post ${i + 1}`}
fill
className="w-full h-full object-cover"
/>
</div>
))}
</div>
<div className="flex justify-center mt-10">
<Link href={"#"}>
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
Lihat Selengkapnya
<ArrowRight size={35} />
</button>
</Link>
</div>
</>
)}
{activeTab === "YOUTUBE" && (
<>
<div className="flex flex-wrap justify-center items-center gap-4">
{youtubePosts.map((img, i) => (
<div
key={i}
className="relative w-full sm:w-[300px] md:w-[350px] lg:w-[400px] h-[400px] sm:h-[450px] md:h-[500px]"
>
<Image
src={img}
alt={`YouTube post ${i + 1}`}
fill
className="w-full h-full object-cover"
/>
</div>
))}
</div>
<div className="flex justify-center mt-10">
<Link href={"#"}>
<button className="bg-[#1F6779] hover:bg-sky-800 text-white px-6 py-3 rounded-md flex items-center gap-2 text-lg font-medium">
Lihat Selengkapnya
<ArrowRight size={35} />
</button>
</Link>
</div>
</>
)}
</section>
);
}

View File

@ -0,0 +1,24 @@
"use client";
import Image from "next/image";
export default function Video() {
return (
<section className="pt-10 bg-white">
<div className="relative mb-10 w-full h-[600px]">
<Image src={"/maintenance.png"} alt="maintenance" fill />
</div>
<div className="relative w-full h-[500px] overflow-hidden">
<iframe
className="w-full h-full"
src="https://www.youtube.com/embed/qEfjAK4gVhU?autoplay=1&mute=1&loop=1&playlist=qEfjAK4gVhU"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</section>
);
}

View File

@ -0,0 +1,89 @@
"use client";
import { useEffect, useState } from "react";
import React, { ReactNode } from "react";
import { SidebarProvider } from "./sidebar-context";
import { ThemeProvider } from "./theme-context";
import { Breadcrumbs } from "./breadcrumbs";
import { BurgerButtonIcon } from "../icons";
import { motion, AnimatePresence } from "framer-motion";
import { RetractingSidebar } from "../landing-page/retracting-sidedar";
export const AdminLayout = ({ children }: { children: ReactNode }) => {
const [isOpen, setIsOpen] = useState(true);
const [hasMounted, setHasMounted] = useState(false);
const updateSidebarData = (newData: boolean) => {
setIsOpen(newData);
};
// Hooks
useEffect(() => {
setHasMounted(true);
}, []);
// Render loading state until mounted
if (!hasMounted) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<ThemeProvider>
<SidebarProvider>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-slate-500 dark:via-slate-800 dark:to-slate-900">
<div className="flex h-screen overflow-hidden">
<RetractingSidebar
sidebarData={isOpen}
updateSidebarData={updateSidebarData}
/>
<AnimatePresence mode="wait">
<motion.div
key="main-content"
className="flex-1 flex flex-col overflow-hidden"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Header */}
<motion.header
className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border-b border-slate-200/60 dark:border-slate-700/60 shadow-sm"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3 }}
>
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center space-x-4">
<button
className="md:hidden p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
onClick={() => updateSidebarData(true)}
>
<BurgerButtonIcon />
</button>
<Breadcrumbs />
</div>
</div>
</motion.header>
{/* Main Content */}
<motion.main
className="flex-1 overflow-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
>
<div className="h-full">{children}</div>
</motion.main>
</motion.div>
</AnimatePresence>
</div>
</div>
</SidebarProvider>
</ThemeProvider>
);
};

View File

@ -0,0 +1,154 @@
"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import {
ArticleIcon,
DashboardIcon,
MagazineIcon,
MasterCategoryIcon,
MasterRoleIcon,
MasterUsersIcon,
StaticPageIcon,
} from "../icons/sidebar-icon";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "../ui/breadcrumb";
import React from "react";
import { motion } from "framer-motion";
export const Breadcrumbs = () => {
const [currentPage, setCurrentPage] = useState<React.Key>("");
const [mounted, setMounted] = useState(false);
const router = useRouter();
const pathname = usePathname();
const pathnameSplit = pathname.split("/");
pathnameSplit.shift();
const pathnameTransformed = pathnameSplit.map((item) => {
const words = item.split("-");
const capitalizedWords = words.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1)
);
return capitalizedWords.join(" ");
});
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
}, [pathnameSplit]);
const handleAction = (key: any) => {
const keyIndex = pathnameSplit.indexOf(key);
const combinedPath = pathnameSplit.slice(0, keyIndex + 1).join("/");
router.push("/" + combinedPath);
};
const getPageIcon = () => {
if (pathname.includes("dashboard")) return <DashboardIcon size={40} />;
if (pathname.includes("article")) return <ArticleIcon size={40} />;
if (pathname.includes("master-category"))
return <MasterCategoryIcon size={40} />;
if (pathname.includes("magazine")) return <MagazineIcon size={40} />;
if (pathname.includes("static-page")) return <StaticPageIcon size={40} />;
if (pathname.includes("master-user")) return <MasterUsersIcon size={40} />;
if (pathname.includes("master-role")) return <MasterRoleIcon size={40} />;
return null;
};
if (!mounted) {
return (
<div className="flex items-center space-x-6">
<div className="w-10 h-10 bg-slate-200 rounded-lg animate-pulse"></div>
<div className="flex flex-col space-y-2">
<div className="h-8 w-32 bg-slate-200 rounded animate-pulse"></div>
<div className="h-4 w-48 bg-slate-200 rounded animate-pulse"></div>
</div>
</div>
);
}
return (
<motion.div
className="flex items-center space-x-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Page Icon */}
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="flex-shrink-0"
>
{getPageIcon()}
</motion.div>
{/* Page Title and Breadcrumbs */}
<div className="flex flex-col space-y-2">
<motion.h1
className="text-2xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-black dark:text-white"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
>
{pathnameTransformed[pathnameTransformed.length - 1]}
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.3 }}
>
<Breadcrumb>
<BreadcrumbList className="flex items-center space-x-2">
{pathnameTransformed
?.filter((item) => item !== "Admin")
.map((item, index, array) => (
<React.Fragment key={pathnameSplit[index]}>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => handleAction(pathnameSplit[index])}
className={`text-sm transition-all duration-200 hover:text-blue-600 ${
pathnameSplit[index] === currentPage
? "font-semibold text-blue-600"
: "text-slate-500 hover:text-slate-700"
}`}
>
{item}
</BreadcrumbLink>
</BreadcrumbItem>
{index < array.length - 1 && (
<BreadcrumbSeparator className="text-slate-400">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</BreadcrumbSeparator>
)}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</motion.div>
</div>
</motion.div>
);
};

View File

@ -0,0 +1,104 @@
"use client";
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ChunkErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
// Check if it's a chunk loading error
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
return { hasError: true, error };
}
return { hasError: false };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Chunk loading error:', error, errorInfo);
// If it's a chunk loading error, try to reload the page
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
this.setState({ hasError: true, error });
}
}
handleRetry = () => {
// Clear the error state and reload the page
this.setState({ hasError: false, error: undefined });
window.location.reload();
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-white to-slate-50">
<div className="text-center p-8 max-w-md">
<div className="mb-6">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<RefreshCw className="w-8 h-8 text-red-600" />
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Chunk Loading Error
</h2>
<p className="text-gray-600 mb-6">
There was an issue loading a part of the application. This usually happens when the application has been updated.
</p>
</div>
<div className="space-y-3">
<Button
onClick={this.handleRetry}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
Reload Application
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="w-full"
>
Go to Homepage
</Button>
</div>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-6 text-left">
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
Error Details (Development)
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto">
{this.state.error.message}
</pre>
</details>
)}
</div>
</div>
);
}
return this.props.children;
}
}
export default ChunkErrorBoundary;

View File

@ -0,0 +1,36 @@
import React from "react";
interface CircularProgressProps {
size?: number;
strokeWidth?: number;
value: number; // 0 to 100
className?: string;
}
export function CircularProgress({ size = 48, strokeWidth = 4, value, className }: CircularProgressProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (value / 100) * circumference;
return (
<svg width={size} height={size} className={className} viewBox={`0 0 ${size} ${size}`}>
<circle className="text-gray-200" stroke="currentColor" strokeWidth={strokeWidth} fill="transparent" r={radius} cx={size / 2} cy={size / 2} />
<circle
className="text-primary"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="transparent"
r={radius}
cx={size / 2}
cy={size / 2}
strokeDasharray={circumference}
strokeDashoffset={offset}
style={{ transition: "stroke-dashoffset 0.35s" }}
/>
<text x="50%" y="50%" dy=".3em" textAnchor="middle" className="text-xs fill-primary font-medium">
{Math.round(value)}%
</text>
</svg>
);
}

View File

@ -0,0 +1,54 @@
import React from "react";
type CircularProgressProps = {
value: number; // antara 0 - 100
size?: number; // diameter lingkaran (px)
strokeWidth?: number;
color?: string;
bgColor?: string;
label?: string;
};
export const CustomCircularProgress = ({
value,
size = 80,
strokeWidth = 8,
color = "#f59e0b", // shadcn's warning color
bgColor = "#e5e7eb", // gray-200
label,
}: CircularProgressProps) => {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const progress = Math.min(Math.max(value, 0), 100); // jaga antara 0 - 100
const offset = circumference - (progress / 100) * circumference;
return (
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
<svg width={size} height={size}>
<circle
stroke={bgColor}
fill="transparent"
strokeWidth={strokeWidth}
r={radius}
cx={size / 2}
cy={size / 2}
/>
<circle
stroke={color}
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
r={radius}
cx={size / 2}
cy={size / 2}
style={{ transition: "stroke-dashoffset 0.35s" }}
/>
</svg>
<span className="absolute text-sm font-semibold text-gray-800 dark:text-white">
{label ?? `${Math.round(progress)}%`}
</span>
</div>
);
};

View File

@ -0,0 +1,84 @@
"use client";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { useEffect, useState } from "react";
export default function CustomPagination(props: { totalPage: number; onPageChange: (data: number) => void }) {
const { totalPage, onPageChange } = props;
const [page, setPage] = useState(1);
useEffect(() => {
onPageChange(page);
}, [page]);
const renderPageNumbers = () => {
const pageNumbers = [];
const halfWindow = Math.floor(5 / 2);
let startPage = Math.max(2, page - halfWindow);
let endPage = Math.min(totalPage - 1, page + halfWindow);
if (endPage - startPage + 1 < 5) {
if (page <= halfWindow) {
endPage = Math.min(totalPage, endPage + (5 - (endPage - startPage + 1)));
} else if (page + halfWindow >= totalPage) {
startPage = Math.max(1, startPage - (5 - (endPage - startPage + 1)));
}
}
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(
<PaginationItem key={i} onClick={() => setPage(i)}>
<PaginationLink className="cursor-pointer" isActive={page === i}>
{i}
</PaginationLink>
</PaginationItem>
);
}
return pageNumbers;
};
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationLink className="cursor-pointer" onClick={() => (page > 10 ? setPage(page - 10) : "")}>
{/* <DoubleArrowLeftIcon /> */}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationPrevious className="cursor-pointer" onClick={() => (page > 1 ? setPage(page - 1) : "")} />
</PaginationItem>
<PaginationItem>
<PaginationLink className="cursor-pointer" onClick={() => setPage(1)} isActive={page === 1}>
{1}
</PaginationLink>
</PaginationItem>
{page > 4 && (
<PaginationItem>
<PaginationEllipsis className="cursor-pointer" onClick={() => setPage(page - 1)} />
</PaginationItem>
)}
{renderPageNumbers()}
{page < totalPage - 3 && (
<PaginationItem>
<PaginationEllipsis className="cursor-pointer" onClick={() => setPage(page + 1)} />
</PaginationItem>
)}
{totalPage > 1 && (
<PaginationItem>
<PaginationLink className="cursor-pointer" onClick={() => setPage(totalPage)} isActive={page === totalPage}>
{totalPage}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationNext className="cursor-pointer" onClick={() => (page < totalPage ? setPage(page + 1) : "")} />
</PaginationItem>
<PaginationItem>
<PaginationLink onClick={() => (page < totalPage - 10 ? setPage(page + 10) : "")}>{/* <DoubleArrowRightIcon /> */}</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}

View File

@ -0,0 +1,58 @@
'use client'
import React, { createContext, useContext, useEffect, useState } from 'react';
interface SidebarContextType {
isOpen: boolean;
toggleSidebar: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== 'undefined') {
const storedValue = localStorage.getItem('sidebarOpen');
return storedValue ? JSON.parse(storedValue) : false;
}
});
const toggleSidebar = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
localStorage.setItem('sidebarOpen', JSON.stringify(isOpen));
}, [isOpen]);
useEffect(() => {
const handleResize = () => {
setIsOpen(window.innerWidth > 768); // Ganti 768 dengan lebar yang sesuai dengan breakpoint Anda
};
handleResize(); // Pastikan untuk memanggil fungsi handleResize saat komponen dimuat
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
{children}
</SidebarContext.Provider>
);
};
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
export const useSidebarContext = () => {
return useContext(SidebarContext);
};

Some files were not shown because too many files have changed in this diff Show More