Initial commit
This commit is contained in:
commit
9116abc611
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function AuthLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <> {children}</>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Login from "@/components/form/login";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function AuthPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Login />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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, "<").replace(/>/g, ">")}</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 J8’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 vehicle’s 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 vehicle’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 Group’s 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 team’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
Loading…
Reference in New Issue