Initial commit
This commit is contained in:
commit
c47de294a9
|
|
@ -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,11 @@
|
|||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div className="h-[96vh] overflow-x-hidden overflow-y-scroll gap-0 grid">
|
||||
<div className="lg:px-4 !w-screen lg:!w-auto">
|
||||
<DashboardContainer />
|
||||
</div>
|
||||
</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,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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 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,34 @@
|
|||
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: "Mikul News",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import Author from "@/components/landing-page/author";
|
||||
import Footer from "@/components/landing-page/footer";
|
||||
import Header from "@/components/landing-page/header";
|
||||
import Latest from "@/components/landing-page/latest";
|
||||
import LatestandPopular from "@/components/landing-page/latest-and-popular";
|
||||
import Navbar from "@/components/landing-page/navbar";
|
||||
import News from "@/components/landing-page/news";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
|
||||
{/* Background fixed tidak ikut scroll */}
|
||||
<div className="fixed top-0 left-0 w-full h-auto z-0">
|
||||
<Image
|
||||
src="/rumput.jpg"
|
||||
alt="Background"
|
||||
width={1450}
|
||||
height={600}
|
||||
className="w-full h-auto object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
|
||||
<Navbar />
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
</div>
|
||||
<Latest />
|
||||
<News />
|
||||
<Author />
|
||||
<LatestandPopular />
|
||||
<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,103 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
const allArticles = Array.from({ length: 20 }, (_, i) => ({
|
||||
title: [
|
||||
"Operasi Keselamatan Musi 2024: Ditlantas Polda Sumsel Himbau Keselamatan de...",
|
||||
"Gelar Operasi Semeru, Kanit Lantas Gencarkan Sosialisasi Keselamatan Berken...",
|
||||
"Kapolsek Sooko sambang dunia pendidikan berikan himbauan kamtibmas di MTS S...",
|
||||
"PATROLI DIALOGIS POLSEK GONDANG TINGKATKAN KEWASPADAAN SEKITAR KEPADA SATPAM",
|
||||
][i % 4],
|
||||
date: "07-03-2024, 07:55",
|
||||
image: `/images/article${(i % 4) + 1}.jpg`,
|
||||
}));
|
||||
|
||||
const articlesPerPage = 4;
|
||||
const totalPages = Math.ceil(allArticles.length / articlesPerPage);
|
||||
|
||||
export default function DashboardRecentArticles() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const paginatedArticles = allArticles.slice(
|
||||
(currentPage - 1) * articlesPerPage,
|
||||
currentPage * articlesPerPage
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="font-semibold text-base">Recent Article</h2>
|
||||
<button className="px-4 py-1 border border-blue-500 text-blue-500 rounded-full hover:bg-blue-50 text-sm">
|
||||
Buat Article
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Article List */}
|
||||
<ul className="space-y-4">
|
||||
{paginatedArticles.map((article, index) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<Image
|
||||
src={article.image}
|
||||
alt={article.title}
|
||||
className="w-16 h-16 rounded object-cover"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium leading-snug line-clamp-2">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">{article.date}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-center mt-6 space-x-1 text-sm">
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||
className="px-2 py-1 rounded text-gray-600"
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
|
||||
{[...Array(totalPages).keys()].slice(0, 5).map((_, idx) => {
|
||||
const page = idx + 1;
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => setCurrentPage(page)}
|
||||
className={`px-3 py-1 rounded-full ${
|
||||
currentPage === page
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<span className="px-2 py-1 text-gray-400">...</span>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full"
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(p + 1, totalPages))}
|
||||
className="px-2 py-1 rounded text-gray-600"
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
|
||||
export default function DashboardStats() {
|
||||
const statsData = [
|
||||
{
|
||||
type: "profile",
|
||||
name: "test akun",
|
||||
role: "admin-mabes",
|
||||
todayPosts: 0,
|
||||
weekPosts: 0,
|
||||
icon: "mdi:account-tie",
|
||||
},
|
||||
{
|
||||
label: "Total post",
|
||||
value: 2363,
|
||||
icon: "mdi:post-outline",
|
||||
},
|
||||
{
|
||||
label: "Total views",
|
||||
value: 80,
|
||||
icon: "ic:baseline-star-rate",
|
||||
},
|
||||
{
|
||||
label: "Total share",
|
||||
value: 1,
|
||||
icon: "mdi:share",
|
||||
},
|
||||
{
|
||||
label: "Total comment",
|
||||
value: 1,
|
||||
icon: "mdi:comment-outline",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col lg:flex-row justify-between gap-4">
|
||||
{statsData.map((item, index) => {
|
||||
if (item.type === "profile") {
|
||||
return (
|
||||
<div key={index} className="bg-white w-full lg:w-[25%] shadow-md rounded-lg p-4 flex flex-col justify-between col-span-1 sm:col-span-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">{item.name}</p>
|
||||
<p className="text-sm text-gray-500">{item.role}</p>
|
||||
</div>
|
||||
<Icon icon={item.icon} className="text-[60px] text-black" />
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-black space-y-1 flex flex-row gap-2">
|
||||
<p>
|
||||
<span className="text-black font-bold text-[18px]">{item.todayPosts} Post</span> Hari ini
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-black font-bold text-[18px]">{item.weekPosts} Post</span> Minggu ini
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="bg-white shadow-md rounded-lg p-4 flex flex-col w-full lg:w-[18%] items-center justify-center">
|
||||
<Icon icon={item.icon} className="text-[45px] text-black mb-2" />
|
||||
<p className="text-sm text-gray-500">{item.label}</p>
|
||||
<p className="text-xl font-bold">{item.value}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
export default function PostRecapTable() {
|
||||
const postsData = [
|
||||
{ name: "POLDA SUMUT", posts: 5304 },
|
||||
{ name: "SATWIL", posts: 4043 },
|
||||
{ name: "POLDA JATENG", posts: 3482 },
|
||||
{ name: "POLDA JATIM", posts: 3138 },
|
||||
{ name: "POLDA SUMSEL", posts: 2677 },
|
||||
{ name: "POLDA JABAR", posts: 1677 },
|
||||
{ name: "POLDA KALTIM", posts: 1565 },
|
||||
{ name: "POLDA RIAU", posts: 1192 },
|
||||
{ name: "POLDA KALBAR", posts: 920 },
|
||||
{ name: "POLDA SULBAR", posts: 730 },
|
||||
{ name: "POLDA METRO JAYA", posts: 707 },
|
||||
{ name: "POLDA BALI", posts: 580 },
|
||||
{ name: "POLDA SULTRA", posts: 375 },
|
||||
{ name: "POLDA MALUKU", posts: 373 },
|
||||
{ name: "POLDA BABEL", posts: 344 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white shadow-md rounded-lg w-full lg:w-[55%]">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4">
|
||||
<h2 className="font-semibold text-xs sm:text-sm">Rekapitulasi Post Berita Polda/Polres Pada Website</h2>
|
||||
<p className="text-xs mt-2 sm:mt-0 sm:text-right">
|
||||
<span className="font-semibold">01-05-2025</span> - <span className="font-semibold">08-05-2025</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scrollable container */}
|
||||
<div className="max-h-[400px] overflow-y-auto overflow-x-auto rounded">
|
||||
<table className="min-w-full text-sm text-left">
|
||||
<thead className="text-black sticky top-0 z-10 bg-white">
|
||||
<tr>
|
||||
<th className="border px-4 py-2 w-10">NO</th>
|
||||
<th className="border px-4 py-2">POLDA/POLRES</th>
|
||||
<th className="border px-4 py-2 text-right">JUMLAH POST BERITA</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-700">
|
||||
{postsData.map((item, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="border px-4 py-2 text-center">{index + 1}</td>
|
||||
<td className="border px-4 py-2">{item.name}</td>
|
||||
<td className="border px-4 py-2 text-right">{item.posts}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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/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/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/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,470 @@
|
|||
"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 Swal from "sweetalert2";
|
||||
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 { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
|
||||
import { saveActivity } from "@/service/activity-log";
|
||||
|
||||
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 {
|
||||
// let response = await emailValidation(data);
|
||||
// if (response?.error) {
|
||||
// error("Username / Password Tidak Sesuai");
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// if (response?.data?.messages[0] === "Continue to setup email") {
|
||||
// setFirstLogin(true);
|
||||
// } else {
|
||||
// setNeedOtp(true);
|
||||
// }
|
||||
|
||||
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",
|
||||
});
|
||||
const resActivity = await saveActivity(
|
||||
{
|
||||
activityTypeId: 1,
|
||||
url: "https://kontenhumas.com/auth",
|
||||
userId: profile?.data?.data?.id,
|
||||
},
|
||||
accessData?.id_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 submitOtp = async () => {
|
||||
// loading();
|
||||
// const validation = await otpValidationLogin({
|
||||
// username: username,
|
||||
// otpCode: otpValue,
|
||||
// });
|
||||
// if (validation?.error) {
|
||||
// error("OTP Tidak Sesuai");
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// const response = await postSignIn({
|
||||
// username: username,
|
||||
// password: password,
|
||||
// });
|
||||
|
||||
// const resProfile = await getProfile(response?.data?.data?.access_token);
|
||||
// const profile = resProfile?.data?.data;
|
||||
|
||||
// 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",
|
||||
// });
|
||||
// const resActivity = await saveActivity(
|
||||
// {
|
||||
// activityTypeId: 1,
|
||||
// url: "https://kontenhumas.com/auth",
|
||||
// userId: profile?.data?.data?.id,
|
||||
// },
|
||||
// accessData?.id_token
|
||||
// );
|
||||
// Cookies.set("profile_picture", profile?.profilePictureUrl, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("uie", profile?.id, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ufne", profile?.fullname, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ulie", profile?.userLevelGroup, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("username", profile?.username, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("urie", profile?.roleId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("roleName", profile?.roleName, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("masterPoldaId", profile?.masterPoldaId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ulne", profile?.userLevelId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("urce", profile?.roleCode, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("email", profile?.email, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// router.push("/admin/dashboard");
|
||||
// Cookies.set("status", "login", {
|
||||
// expires: 1,
|
||||
// });
|
||||
// close();
|
||||
// };
|
||||
|
||||
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="flex flex-row h-full">
|
||||
<div className="hidden md:flex w-full md:w-3/5 items-center justify-center bg-white p-6">
|
||||
<Link href={"/"}>
|
||||
<img src="/mikul.png" alt="logo" className="max-w-full h-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
{isFirstLogin ? (
|
||||
<div className="bg-black w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
<p className="text-[72px] text-[#ce3b28] font-semibold mb-10">
|
||||
Setting Account
|
||||
</p>
|
||||
{/* <p className="my-2 text-white">Email Lama</p> */}
|
||||
{/* <Input isRequired type="email" label="" placeholder="" className="my-2" classNames={{ input: "rounded-md", inputWrapper: "rounded-md" }} value={oldEmail} onValueChange={setOldEmail} /> */}
|
||||
<div className="space-y-2 my-4">
|
||||
<Label htmlFor="old-email" className="text-sm font-medium">
|
||||
Email Lama
|
||||
</Label>
|
||||
<Input
|
||||
id="old-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email lama"
|
||||
className="rounded-md"
|
||||
value={oldEmail}
|
||||
onChange={(e) => setOldEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* <p className="my-2 text-white">Email Baru</p> */}
|
||||
{/* <Input isRequired type="email" label="" placeholder="" className="my-2" classNames={{ input: "rounded-md", inputWrapper: "rounded-md" }} value={newEmail} onValueChange={setNewEmail} /> */}
|
||||
<div className="my-2">
|
||||
<Label htmlFor="new-email" className="text-white">
|
||||
Email Baru
|
||||
</Label>
|
||||
<Input
|
||||
id="new-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email baru"
|
||||
className="text-white mt-1 rounded-md"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-fit bg-[#DD8306] rounded-md font-semibold my-3 text-white hover:bg-[#c87505]"
|
||||
onClick={submitCheckEmail}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
) : needOtp ? (
|
||||
<div className="bg-black w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
{/* <p className="text-[72px] text-[#DD8306] font-semibold mb-10">Submit OTP</p>
|
||||
<p className="my-2 text-white">OTP</p>
|
||||
<Input length={6} value={otpValue} onValueChange={setOtpValue} />
|
||||
|
||||
<Button size="lg" className="w-fit bg-[#DD8306] rounded-md font-semibold my-3 text-white" onPress={submitOtp}>
|
||||
Submit
|
||||
</Button>
|
||||
<div className="flex justify-between md:justify-end my-2 text-white">
|
||||
<Link href={`/`} className="text-[#DD8306] cursor-pointer md:hidden">
|
||||
Beranda
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
) : isResetPassword ? (
|
||||
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
<p className="text-[72px] text-[#ce3b28] font-semibold mb-10">
|
||||
Reset Password
|
||||
</p>
|
||||
<Label htmlFor="username" className="text-white">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Masukkan username"
|
||||
className="my-2 rounded-md text-white"
|
||||
value={checkUsernameValue}
|
||||
onChange={(e) => setCheckUsernameValue(e.target.value.trim())}
|
||||
onPaste={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white hover:bg-[#c87505]"
|
||||
onClick={checkUsername}
|
||||
disabled={checkUsernameValue === ""}
|
||||
>
|
||||
Check Username
|
||||
</Button>
|
||||
<div className="flex justify-between md:justify-end my-2 text-white">
|
||||
<Link
|
||||
href={`/`}
|
||||
className="text-[#DD8306] cursor-pointer md:hidden"
|
||||
>
|
||||
Beranda
|
||||
</Link>
|
||||
|
||||
<a
|
||||
className="text-[#DD8306] cursor-pointer"
|
||||
onClick={() => setIsResetPassword(false)}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-[#31942E] w-full md:w-2/5 p-8 md:px-24 flex flex-col justify-center min-h-screen">
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
Selamat Datang di Portal Mikul News
|
||||
</div>
|
||||
<div className="text-sm font-semibold pb-4 text-white">
|
||||
Silahkan Login untuk Melihat informasi serta untuk mengetahui
|
||||
status permintaan informasi dan keberatan yang sudah diajukan.
|
||||
</div>
|
||||
|
||||
<Label htmlFor="username" className="my-2 text-white">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Masukkan username"
|
||||
className="my-2 rounded-md text-white"
|
||||
value={username}
|
||||
onChange={(e) => setValUsername(e.target.value.trim())}
|
||||
onPaste={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
/>
|
||||
|
||||
<Label htmlFor="password" className="my-2 text-white">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
required
|
||||
type={isVisible ? "text" : "password"}
|
||||
placeholder="Masukkan password"
|
||||
className="pr-10 rounded-md text-white"
|
||||
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"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeSlashFilledIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeFilledIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full text-[#31942E] border-2 border-[#000000] bg-white rounded-md font-semibold my-3"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-between md:justify-end my-2 text-white text-sm">
|
||||
<Link href={`/`} className="text-red-500 md:hidden">
|
||||
Beranda
|
||||
</Link>
|
||||
<a
|
||||
className="text-red-500 cursor-pointer"
|
||||
onClick={() => setIsResetPassword(true)}
|
||||
>
|
||||
Reset Password
|
||||
</a>
|
||||
</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,46 @@
|
|||
import Image from "next/image";
|
||||
import { Link, Mail, MailIcon, Share2 } from "lucide-react";
|
||||
|
||||
export default function Author() {
|
||||
return (
|
||||
<section className="w-full bg-white py-6">
|
||||
<p className="mx-10 text-2xl mb-4">AUTHOR</p>
|
||||
<div className=" border border-black p-6 flex items-center gap-6 max-w-[1200px] mx-auto">
|
||||
{/* Foto Profil */}
|
||||
<div className="w-20 h-20 relative ">
|
||||
<Image
|
||||
src="/author.png" // Ganti dengan path gambar kamu
|
||||
alt="Author"
|
||||
fill
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info Author */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Admin</h3>
|
||||
|
||||
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
||||
{/* Button lihat semua post */}
|
||||
<button className="text-sm font-medium text-white hover:underline bg-[#655997] py-1 px-5 rounded-xl">
|
||||
Lihat Semua Pos
|
||||
</button>
|
||||
|
||||
<div className="bg-[#655997] rounded-full p-1">
|
||||
<MailIcon
|
||||
size={18}
|
||||
className="text-white hover:text-black cursor-pointer "
|
||||
></MailIcon>
|
||||
</div>
|
||||
<div className="bg-[#655997] rounded-full p-1">
|
||||
<Link
|
||||
size={18}
|
||||
className="text-white hover:text-black cursor-pointer "
|
||||
></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// components/Footer.tsx
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className=" text-[#FFFFFFCC] text-sm font-sans ">
|
||||
<div className="max-w-7xl mx-auto py-10 bg-[#09282C] px-8">
|
||||
{/* Top Menu Links */}
|
||||
<div className="flex flex-col md:flex-row justify-center md:justify-between gap-3">
|
||||
<div className="w-full md:w-2/12">
|
||||
<p className="text-sm text-gray-400 mt-5">
|
||||
© 2025{" "}
|
||||
<span className="text-xs text-white font-semibold">JNews</span>-
|
||||
Premium WordPress news & magazine theme by{" "}
|
||||
<span className="text-white font-semibold">Jegtheme</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full md:w-6/12">
|
||||
<h2 className="border-b-2 mb-5"></h2>
|
||||
<div className="flex items-start flex-wrap justify-start md:justify-start gap-2 md:gap-3 text-xs text-white font-semibold">
|
||||
{["Beranda ", "Pembangunan", "Kesehatan", "Berita Warga"].map(
|
||||
(item, idx, arr) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="flex items-center gap-2 whitespace-nowrap"
|
||||
>
|
||||
<a href="#" className="hover:underline">
|
||||
{item}
|
||||
</a>
|
||||
{idx !== arr.length - 1 && (
|
||||
<span className="text-white">/</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className=" w-full md:w-3/12">
|
||||
<div className="flex flex-col justify-center md:justify-end gap-2 md:gap-3 text-xs text-[#FFFFFF]">
|
||||
<p className="text-xs font-bold text-red-600 mb-2 md:mb-0 w-10/12 text-start">
|
||||
Follow Us
|
||||
</p>
|
||||
<h2 className="border-b-2 "></h2>
|
||||
<div className="flex gap-6 text-white text-lg">
|
||||
<Link href="#" aria-label="Facebook">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
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>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Twitter">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.91 20.889c8.302 0 12.845-6.885 12.845-12.845c0-.193 0-.387-.009-.58A9.2 9.2 0 0 0 23 5.121a9.2 9.2 0 0 1-2.597.713a4.54 4.54 0 0 0 1.99-2.5a9 9 0 0 1-2.87 1.091A4.5 4.5 0 0 0 16.23 3a4.52 4.52 0 0 0-4.516 4.516c0 .352.044.696.114 1.03a12.82 12.82 0 0 1-9.305-4.718a4.526 4.526 0 0 0 1.4 6.03a4.6 4.6 0 0 1-2.043-.563v.061a4.524 4.524 0 0 0 3.62 4.428a4.4 4.4 0 0 1-1.189.159q-.435 0-.845-.08a4.51 4.51 0 0 0 4.217 3.135a9.05 9.05 0 0 1-5.608 1.936A9 9 0 0 1 1 18.873a12.84 12.84 0 0 0 6.91 2.016"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Google" className="text-[#F5F5F5]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M7.796 14.333v-2.618h7.211c.066.382.12.763.12 1.265c0 4.364-2.923 7.462-7.33 7.462A7.63 7.63 0 0 1 .16 12.806a7.63 7.63 0 0 1 7.636-7.637c2.062 0 3.786.753 5.117 1.997L10.84 9.162c-.567-.546-1.56-1.178-3.044-1.178c-2.607 0-4.734 2.16-4.734 4.822s2.127 4.821 4.734 4.821c3.022 0 4.157-2.17 4.331-3.294zm13.27-2.6H23.2v2.134h-2.133V16h-2.134v-2.133H16.8v-2.134h2.133V9.6h2.134z"
|
||||
// clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Pinterest">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
id="akarIconsPinterestFill0"
|
||||
fill="#fff"
|
||||
d="M0 0h24v24H0z"
|
||||
/>
|
||||
</defs>
|
||||
<g fill="none">
|
||||
<g
|
||||
// clip-path="url(#akarIconsPinterestFill1)"
|
||||
>
|
||||
<g
|
||||
// clip-path="url(#akarIconsPinterestFill2)"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0 12c0 5.123 3.211 9.497 7.73 11.218c-.11-.937-.227-2.482.025-3.566c.217-.932 1.401-5.938 1.401-5.938s-.357-.715-.357-1.774c0-1.66.962-2.9 2.161-2.9c1.02 0 1.512.765 1.512 1.682c0 1.025-.653 2.557-.99 3.978c-.281 1.189.597 2.159 1.769 2.159c2.123 0 3.756-2.239 3.756-5.471c0-2.861-2.056-4.86-4.991-4.86c-3.398 0-5.393 2.549-5.393 5.184c0 1.027.395 2.127.889 2.726a.36.36 0 0 1 .083.343c-.091.378-.293 1.189-.332 1.355c-.053.218-.173.265-.4.159c-1.492-.694-2.424-2.875-2.424-4.627c0-3.769 2.737-7.229 7.892-7.229c4.144 0 7.365 2.953 7.365 6.899c0 4.117-2.595 7.431-6.199 7.431c-1.211 0-2.348-.63-2.738-1.373c0 0-.599 2.282-.744 2.84c-.282 1.084-1.064 2.456-1.549 3.235C9.584 23.815 10.77 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0S0 5.373 0 12"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="akarIconsPinterestFill1">
|
||||
<use href="#akarIconsPinterestFill0" />
|
||||
</clipPath>
|
||||
<clipPath id="akarIconsPinterestFill2">
|
||||
<use href="#akarIconsPinterestFill0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link href="#" aria-label="Vk">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M23.45 5.948c.166-.546 0-.948-.795-.948H20.03c-.668 0-.976.347-1.143.73c0 0-1.335 3.196-3.226 5.272c-.612.602-.89.793-1.224.793c-.167 0-.418-.191-.418-.738V5.948c0-.656-.184-.948-.74-.948H9.151c-.417 0-.668.304-.668.593c0 .621.946.765 1.043 2.513v3.798c0 .833-.153.984-.487.984c-.89 0-3.055-3.211-4.34-6.885C4.45 5.288 4.198 5 3.527 5H.9c-.75 0-.9.347-.9.73c0 .682.89 4.07 4.145 8.551C6.315 17.341 9.37 19 12.153 19c1.669 0 1.875-.368 1.875-1.003v-2.313c0-.737.158-.884.687-.884c.39 0 1.057.192 2.615 1.667C19.11 18.216 19.403 19 20.405 19h2.625c.75 0 1.126-.368.91-1.096c-.238-.724-1.088-1.775-2.215-3.022c-.612-.71-1.53-1.475-1.809-1.858c-.389-.491-.278-.71 0-1.147c0 0 3.2-4.426 3.533-5.929"
|
||||
// clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Wifi">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 21q-1.05 0-1.775-.725T9.5 18.5t.725-1.775T12 16t1.775.725t.725 1.775t-.725 1.775T12 21m-5.65-5.65l-2.1-2.15q1.475-1.475 3.463-2.337T12 10t4.288.875t3.462 2.375l-2.1 2.1q-1.1-1.1-2.55-1.725T12 13t-3.1.625t-2.55 1.725M2.1 11.1L0 9q2.3-2.35 5.375-3.675T12 4t6.625 1.325T24 9l-2.1 2.1q-1.925-1.925-4.462-3.012T12 7T6.563 8.088T2.1 11.1"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
"use client";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { getListArticle } from "@/service/article";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
type Article = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
categoryName: string;
|
||||
createdAt: string;
|
||||
createdByName: string;
|
||||
thumbnailUrl: string;
|
||||
categories: {
|
||||
title: string;
|
||||
}[];
|
||||
files: {
|
||||
file_url: string;
|
||||
file_alt: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function HeroNewsSection() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
const [showData, setShowData] = useState("5");
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||
const [startDateValue, setStartDateValue] = useState({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, startDateValue, selectedCategories]);
|
||||
|
||||
async function initState() {
|
||||
// loading();
|
||||
const req = {
|
||||
limit: showData,
|
||||
page,
|
||||
search,
|
||||
categorySlug: Array.from(selectedCategories).join(","),
|
||||
sort: "desc",
|
||||
sortBy: "created_at",
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await getListArticle(req);
|
||||
setArticles(res?.data?.data || []);
|
||||
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="max-w-7xl mx-auto bg-white">
|
||||
<div className="flex items-center bg-[#F2F4F3] w-full overflow-hidden mb-4 py-6 px-8">
|
||||
<Image
|
||||
src="/mikul.png"
|
||||
alt="Background"
|
||||
width={272}
|
||||
height={90}
|
||||
className="w-[272px] h-[90px] object-cover border"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pb-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-0.5 m-8 ">
|
||||
{/* Artikel Utama */}
|
||||
{articles.length > 0 && (
|
||||
<div className="md:col-span-2 lg:col-span-3 relative">
|
||||
<Image
|
||||
src={
|
||||
articles[0].thumbnailUrl ||
|
||||
articles[0]?.files?.[0]?.file_url ||
|
||||
"/default-image.jpg"
|
||||
}
|
||||
alt={articles[0].title}
|
||||
width={800}
|
||||
height={500}
|
||||
className="w-full h-full max-h-[460px] object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/50 to-transparent p-6 flex flex-col justify-end">
|
||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block mb-2 uppercase w-[130px]">
|
||||
{articles[0].categories?.[0]?.title || "TANPA KATEGORI"}
|
||||
</span>
|
||||
<h2 className="text-sm md:text-xl lg:text-2xl font-bold text-white leading-snug mb-2 w-full md:w-9/12">
|
||||
{articles[0].title}
|
||||
</h2>
|
||||
<p className="text-white text-xs">
|
||||
{articles[0].createdByName} -{" "}
|
||||
{new Date(articles[0].createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Artikel Tambahan */}
|
||||
<div className="md:col-span-1 lg:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{articles.slice(1, 5).map((article, index) => (
|
||||
<div key={index} className="relative">
|
||||
<Image
|
||||
src={
|
||||
article.thumbnailUrl ||
|
||||
article?.files?.[0]?.file_url ||
|
||||
"/default-image.jpg"
|
||||
}
|
||||
alt={article.title}
|
||||
width={400}
|
||||
height={240}
|
||||
className="w-full h-56 object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-4 flex flex-col justify-end">
|
||||
<span className="text-xs bg-yellow-400 text-black px-2 py-0.5 inline-block uppercase w-[130px]">
|
||||
{article.categoryName || "TANPA KATEGORI"}
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-white leading-snug mb-1">
|
||||
{article.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Iklan / Gambar Bawah */}
|
||||
<div className="relative mt-10 mb-2 h-[188px] overflow-hidden flex items-center mx-8 border my-8">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import Image from "next/image";
|
||||
|
||||
export default function LateNews() {
|
||||
return (
|
||||
<section className="bg-white py-10 px-4 md:px-10 w-full">
|
||||
<div className="max-w-screen-6xl mx-auto flex flex-col lg:flex-row lg:justify-between gap-5">
|
||||
{/* Left: News Section */}
|
||||
<div className="w-full lg:w-[750px]">
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
FEATURED
|
||||
</h2>
|
||||
</div>
|
||||
<div className="relative w-[1111px] max-w-full h-[300px] overflow-hidden flex items-center mx-auto border my-6 rounded">
|
||||
<Image
|
||||
src="/kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
import Image from "next/image";
|
||||
|
||||
const data1 = {
|
||||
main: {
|
||||
image: "/jumbo.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Industri Animasi Indonesia Melesat: “Jumbo” Masuk Daftar Film Terlaris Sepanjang Masa",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Industri film animasi Indonesia menunjukkan kemajuan signifikan dalam beberapa tahun terakhir. Tak hanya sukses dari sisi kreativitas dan produksi,...",
|
||||
},
|
||||
left: [
|
||||
{
|
||||
image: "/jumbo.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Industri Animasi Indonesia Melesat: “Jumbo” Masuk Daftar Film Terlaris Sepanjang Masa",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Industri film animasi Indonesia menunjukkan kemajuan signifikan dalam beberapa tahun terakhir. Tak hanya sukses dari sisi kreativitas dan produksi,...",
|
||||
},
|
||||
],
|
||||
topRight: [
|
||||
{
|
||||
image: "/bimantoro.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Bimantoro Wiyono Apresiasi Keberhasilan Atur Mudik dan Arus Balik Lebaran 2025",
|
||||
author: "SALMA HN",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Jakarta - Rekrutmen Bersama BUMN (RBB BUMN) 2025 kini resmi dibuka mulai hari ini, Senin, 10 Maret 2025. Pelamar yang...",
|
||||
},
|
||||
],
|
||||
topRightMain: [
|
||||
{
|
||||
image: "/bimantoro.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Bimantoro Wiyono Apresiasi Keberhasilan Atur Mudik dan Arus Balik Lebaran 2025",
|
||||
author: "SALMA HN",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Jakarta - Rekrutmen Bersama BUMN (RBB BUMN) 2025 kini resmi dibuka mulai hari ini, Senin, 10 Maret 2025. Pelamar yang...",
|
||||
},
|
||||
{
|
||||
image: "/cpns.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Polri Dapat Meningkatkan Transparansi: Paparan Fathul Ulum tentang Keterbukaan Informasi",
|
||||
author: "SALMA HN",
|
||||
date: "7 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"JAKARTA - Program Rekrutmen Bersama BUMN (RBB) 2025 resmi dibuka pada Jumat...",
|
||||
},
|
||||
{
|
||||
image: "/bpnt.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Tutorial Cek Status Penerima BPNT 2025 Melalui HP",
|
||||
author: "SALMA HN",
|
||||
date: "7 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"JAKARTA - Program Rekrutmen Bersama BUMN (RBB) 2025 resmi dibuka pada Jumat...",
|
||||
},
|
||||
],
|
||||
bottomMid: [
|
||||
{
|
||||
image: "/vvip.jpg",
|
||||
title:
|
||||
"Pembangunan Bandara VVIP IKN Berjalan Lancar, Ditargetkan Rampung Maret 2025",
|
||||
date: "6 MARET 2025",
|
||||
},
|
||||
{
|
||||
image: "/pmk.png",
|
||||
title: "PMK 11/2025 Ubah Ketentuan PPN Besaran Tertentu, Ini Rinciannya",
|
||||
date: "11 FEBRUARI 2025",
|
||||
},
|
||||
{
|
||||
image: "/cpns.jpg",
|
||||
title:
|
||||
"Hasil Kelulusan CPNS 2024 Diumumkan, Simak Arti Kode dan Cara Mengecek Hasilnya",
|
||||
date: "17 FEBRUARI 2025",
|
||||
},
|
||||
],
|
||||
bottomRightData: [
|
||||
{
|
||||
image: "/iums.png",
|
||||
category: "INTERNASIONAL",
|
||||
title:
|
||||
"IUMS Serukan Boikot Produk Pendukung Israel, PMII Rilis Daftar Merek Terkait",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Persatuan Cendekiawan Muslim Internasional atau International Union of Muslim Scholars (IUMS) yang berbasis di Qatar mengeluarkan fatwa terkait konflik...",
|
||||
},
|
||||
{
|
||||
image: "/jateng.png",
|
||||
category: "INTERNASIONAL",
|
||||
title:
|
||||
"Tiga Polisi Jaga Tahanan di Polda Jateng Ditahan Akibat Dugaan Pungli",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Persatuan Cendekiawan Muslim Internasional atau International Union of Muslim Scholars (IUMS) yang berbasis di Qatar mengeluarkan fatwa terkait konflik...",
|
||||
},
|
||||
{
|
||||
image: "/perang.png",
|
||||
category: "INTERNASIONAL",
|
||||
title:
|
||||
"Perang Dagang AS-China Meningkat, Indonesia Terancam Tekanan Ekonomi Ganda",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Persatuan Cendekiawan Muslim Internasional atau International Union of Muslim Scholars (IUMS) yang berbasis di Qatar mengeluarkan fatwa terkait konflik...",
|
||||
},
|
||||
{
|
||||
image: "/gaza.png",
|
||||
category: "INTERNASIONAL",
|
||||
title:
|
||||
"Fatwa Ulama Dunia Serukan Blokade Total Terhadap Israel Demi Bela Gaza",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Persatuan Cendekiawan Muslim Internasional atau International Union of Muslim Scholars (IUMS) yang berbasis di Qatar mengeluarkan fatwa terkait konflik...",
|
||||
},
|
||||
],
|
||||
bottomRight: [
|
||||
{
|
||||
image: "/iums.png",
|
||||
category: "INTERNASIONAL",
|
||||
title:
|
||||
"IUMS Serukan Boikot Produk Pendukung Israel, PMII Rilis Daftar Merek Terkait",
|
||||
author: "christine natalia",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Mikulnews.com - Persatuan Cendekiawan Muslim Internasional atau International Union of Muslim Scholars (IUMS) yang berbasis di Qatar mengeluarkan fatwa terkait konflik...",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Disusun ulang agar cocok dengan struktur komponen
|
||||
const data = {
|
||||
leftMain: data1.main,
|
||||
leftList: data1.left,
|
||||
bottomRightList: data1.bottomRightData,
|
||||
centerMain: data1.topRight[0],
|
||||
centerList: data1.bottomMid.slice(0, 3),
|
||||
rightMain: data1.topRightMain[0],
|
||||
rightList: data1.bottomRight.slice(0, 3),
|
||||
};
|
||||
|
||||
export default function LatestandPopular() {
|
||||
return (
|
||||
<section className="bg-white py-10 px-4 md:px-10 w-full">
|
||||
<div className="max-w-screen-6xl mx-auto flex flex-col lg:flex-row lg:justify-between gap-5">
|
||||
{/* Left: News Section */}
|
||||
<div className="w-full lg:w-[750px]">
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
Latest Post
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 ">
|
||||
{/* === LEFT COLUMN === */}
|
||||
<div className=" w-full">
|
||||
{data.leftList.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="relative w-full aspect-video mb-5">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<span className="absolute bottom-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
||||
{item.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className=" text-black">
|
||||
<h3 className=" font-semibold text-base mb-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-[#999999] text-sm font-serif">
|
||||
{item.excerpt}
|
||||
</p>
|
||||
<p className="text-xs text-[#999999] mb-2 flex items-center gap-2">
|
||||
by <span className="text-[#31942E]">{item?.author}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{item.date}{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>{" "}
|
||||
0{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 8C7.664 8 1.25 15.344 1.25 15.344L.656 16l.594.656s5.848 6.668 13.625 7.282c.371.046.742.062 1.125.062s.754-.016 1.125-.063c7.777-.613 13.625-7.28 13.625-7.28l.594-.657l-.594-.656S24.336 8 16 8m0 2c2.203 0 4.234.602 6 1.406A6.9 6.9 0 0 1 23 15a6.995 6.995 0 0 1-6.219 6.969c-.02.004-.043-.004-.062 0c-.239.011-.477.031-.719.031c-.266 0-.523-.016-.781-.031A6.995 6.995 0 0 1 9 15c0-1.305.352-2.52.969-3.563h-.031C11.717 10.617 13.773 10 16 10m0 2a3 3 0 1 0 .002 6.002A3 3 0 0 0 16 12m-8.75.938A9 9 0 0 0 7 15c0 1.754.5 3.395 1.375 4.781A23.2 23.2 0 0 1 3.531 16a24 24 0 0 1 3.719-3.063zm17.5 0A24 24 0 0 1 28.469 16a23.2 23.2 0 0 1-4.844 3.781A8.93 8.93 0 0 0 25 15c0-.715-.094-1.398-.25-2.063z"
|
||||
/>
|
||||
</svg>{" "}
|
||||
19
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* === RIGHT COLUMN === */}
|
||||
<div className=" w-full">
|
||||
{data.rightList.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className="relative w-full aspect-video mb-5">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<span className="absolute bottom-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
||||
{item.category}
|
||||
</span>
|
||||
</div>
|
||||
<div className=" text-black">
|
||||
<h3 className=" font-semibold text-base mb-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-[#999999] text-sm font-serif">
|
||||
{item.excerpt}
|
||||
</p>
|
||||
<p className="text-xs text-[#999999] mb-2 flex items-center gap-2">
|
||||
by <span className="text-[#31942E]">{item?.author}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{item.date}{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>{" "}
|
||||
0{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 8C7.664 8 1.25 15.344 1.25 15.344L.656 16l.594.656s5.848 6.668 13.625 7.282c.371.046.742.062 1.125.062s.754-.016 1.125-.063c7.777-.613 13.625-7.28 13.625-7.28l.594-.657l-.594-.656S24.336 8 16 8m0 2c2.203 0 4.234.602 6 1.406A6.9 6.9 0 0 1 23 15a6.995 6.995 0 0 1-6.219 6.969c-.02.004-.043-.004-.062 0c-.239.011-.477.031-.719.031c-.266 0-.523-.016-.781-.031A6.995 6.995 0 0 1 9 15c0-1.305.352-2.52.969-3.563h-.031C11.717 10.617 13.773 10 16 10m0 2a3 3 0 1 0 .002 6.002A3 3 0 0 0 16 12m-8.75.938A9 9 0 0 0 7 15c0 1.754.5 3.395 1.375 4.781A23.2 23.2 0 0 1 3.531 16a24 24 0 0 1 3.719-3.063zm17.5 0A24 24 0 0 1 28.469 16a23.2 23.2 0 0 1-4.844 3.781A8.93 8.93 0 0 0 25 15c0-.715-.094-1.398-.25-2.063z"
|
||||
/>
|
||||
</svg>{" "}
|
||||
19
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center space-x-2 pt-6">
|
||||
<button className="px-3 py-1 border text-white rounded bg-[#31942E]">
|
||||
1
|
||||
</button>
|
||||
<button className="px-3 py-1 border border-gray-300 text-black rounded hover:bg-gray-100">
|
||||
2
|
||||
</button>
|
||||
<button className="px-3 py-1 border border-gray-300 text-black rounded hover:bg-gray-100">
|
||||
3
|
||||
</button>
|
||||
<span className="text-gray-400">…</span>
|
||||
<button className="px-3 py-1 border border-gray-300 text-black rounded hover:bg-gray-100">
|
||||
19
|
||||
</button>
|
||||
<button className="px-3 py-1 border border-gray-300 text-black rounded hover:bg-gray-100">
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="w-full lg:w-[345px]">
|
||||
<div className="">
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
Most Popular
|
||||
</h2>
|
||||
<div className=" w-full">
|
||||
<div className="relative w-full aspect-video mb-5">
|
||||
<Image
|
||||
src={data1.main.image}
|
||||
alt={data1.main.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
<div className="absolute bottom-0.5 left-2 text-white">
|
||||
<h3 className=" font-semibold text-base mb-1">
|
||||
{data1.main.title}
|
||||
</h3>
|
||||
<p className=" text-xs mb-2 flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{data1.main.date}{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{data.bottomRightList.map((item, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 flex gap-2 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{item.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 my-6">
|
||||
<div className="flex-1 h-px bg-gray-300" />
|
||||
<p className="text-center border px-2 py-2 w-[110px] text-xs font-medium cursor-pointer">
|
||||
LOAD MORE
|
||||
</p>
|
||||
<div className="flex-1 h-px bg-gray-300" />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
import {
|
||||
Clock,
|
||||
Facebook,
|
||||
Instagram,
|
||||
RefreshCcwIcon,
|
||||
ThumbsUpIcon,
|
||||
Twitter,
|
||||
UserRoundPlus,
|
||||
Youtube,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
const data1 = {
|
||||
main: {
|
||||
image: "/stadion.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Deretan Stadion Terbesar di ASEAN, Kebanggaan Negara-Negara Asia Tenggara",
|
||||
author: "SALMA HN",
|
||||
date: "24 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"KAKORLANTAS POLRI, Cilegon. Kakorlantas Polri Irjen Pol Drs. Agus Suryonogroho S.H, M.Hum bersama Menteri Perhubungan (Menhub) Dudy Purwagandhi meninjau kondisi...",
|
||||
},
|
||||
left: [
|
||||
{
|
||||
image: "/menkes.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Menkes: Kecelakaan Lalu Lintas Turun 45%, Apresiasi Seluruh Stakeholder",
|
||||
author: "SALMA HN",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Jakarta - Rekrutmen Bersama BUMN (RBB BUMN) 2025 kini resmi dibuka mulai hari ini...",
|
||||
},
|
||||
{
|
||||
image: "/kakorlantas.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Korlantas Polri: One Way Lokal dan Contraflow untuk Kelancaran Arus Balik",
|
||||
author: "SALMA HN",
|
||||
date: "7 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"JAKARTA - Program Rekrutmen Bersama BUMN (RBB) 2025 resmi dibuka pada Jumat...",
|
||||
},
|
||||
],
|
||||
topRight: [
|
||||
{
|
||||
image: "/bimantoro.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Bimantoro Wiyono Apresiasi Keberhasilan Atur Mudik dan Arus Balik Lebaran 2025",
|
||||
author: "SALMA HN",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Jakarta - Rekrutmen Bersama BUMN (RBB BUMN) 2025 kini resmi dibuka mulai hari ini, Senin, 10 Maret 2025. Pelamar yang...",
|
||||
},
|
||||
],
|
||||
topRightMain: [
|
||||
{
|
||||
image: "/bimantoro.png",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Bimantoro Wiyono Apresiasi Keberhasilan Atur Mudik dan Arus Balik Lebaran 2025",
|
||||
author: "SALMA HN",
|
||||
date: "10 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"Jakarta - Rekrutmen Bersama BUMN (RBB BUMN) 2025 kini resmi dibuka mulai hari ini, Senin, 10 Maret 2025. Pelamar yang...",
|
||||
},
|
||||
{
|
||||
image: "/cpns.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Polri Dapat Meningkatkan Transparansi: Paparan Fathul Ulum tentang Keterbukaan Informasi",
|
||||
author: "SALMA HN",
|
||||
date: "7 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"JAKARTA - Program Rekrutmen Bersama BUMN (RBB) 2025 resmi dibuka pada Jumat...",
|
||||
},
|
||||
{
|
||||
image: "/bpnt.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Tutorial Cek Status Penerima BPNT 2025 Melalui HP",
|
||||
author: "SALMA HN",
|
||||
date: "7 MARET 2025",
|
||||
comments: 0,
|
||||
excerpt:
|
||||
"JAKARTA - Program Rekrutmen Bersama BUMN (RBB) 2025 resmi dibuka pada Jumat...",
|
||||
},
|
||||
],
|
||||
bottomMid: [
|
||||
{
|
||||
image: "/vvip.jpg",
|
||||
title:
|
||||
"Pembangunan Bandara VVIP IKN Berjalan Lancar, Ditargetkan Rampung Maret 2025",
|
||||
date: "6 MARET 2025",
|
||||
},
|
||||
{
|
||||
image: "/pmk.png",
|
||||
title: "PMK 11/2025 Ubah Ketentuan PPN Besaran Tertentu, Ini Rinciannya",
|
||||
date: "11 FEBRUARI 2025",
|
||||
},
|
||||
{
|
||||
image: "/cpns.jpg",
|
||||
title:
|
||||
"Hasil Kelulusan CPNS 2024 Diumumkan, Simak Arti Kode dan Cara Mengecek Hasilnya",
|
||||
date: "17 FEBRUARI 2025",
|
||||
},
|
||||
],
|
||||
bottomRight: [
|
||||
{
|
||||
image: "/kapolri.jpg",
|
||||
title:
|
||||
"Kapolri dan Kakorlantas Pantau Arus Balik di KM 456 Salatiga, Pastikan Arus Balik Aman",
|
||||
date: "6 MARET 2025",
|
||||
},
|
||||
{
|
||||
image: "/timnas.png",
|
||||
title:
|
||||
"Menang atas Bahrain, Indonesia Masih Harus Waspada Ancaman China dan Jepang",
|
||||
date: "11 FEBRUARI 2025",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Disusun ulang agar cocok dengan struktur komponen
|
||||
const data = {
|
||||
leftMain: data1.main,
|
||||
leftList: data1.left,
|
||||
centerMain: data1.topRight[0],
|
||||
centerList: data1.bottomMid.slice(0, 3),
|
||||
rightMain: data1.topRightMain[0],
|
||||
rightList: data1.bottomRight.slice(0, 3),
|
||||
};
|
||||
|
||||
const popularPosts = [
|
||||
{
|
||||
id: 1,
|
||||
image: "/investasi.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"Polda Gorontalo buka layanan Aduan Pinjaman Online dan Investasi Ilegal",
|
||||
excerpt:
|
||||
"Jakarta – Banyaknya korban pinjaman Online yang terjadi akhir-akhir ini, membuat Kapolda Gorontalo Irjen Pol....",
|
||||
author: "SALMA HASNA",
|
||||
date: "25 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: "/kkb.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Mematahkan Kelompok Kriminal Bersenjata (KKB) di Papua",
|
||||
excerpt:
|
||||
"Penanganan keamanan di Papua harus dilakukan hati-hati karena di Papua merupakan kombinasi antara kepentingan ideologis...",
|
||||
author: "SALMA HASNA",
|
||||
date: "24 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: "/mural.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Polri Gelar Lomba Safari Bhayangkara Mural",
|
||||
excerpt:
|
||||
"JAKARTA - PolrI bersama mitra kembali menggelar lomba mural jilid kedua yang diberi nama Safari...",
|
||||
author: "SALMA HASNA",
|
||||
date: "25 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Latest() {
|
||||
return (
|
||||
<section className="bg-white py-10 px-4 md:px-10 w-full">
|
||||
<div className="max-w-screen-6xl mx-auto flex flex-col lg:flex-row lg:justify-between gap-5">
|
||||
{/* Left: News Section */}
|
||||
<div className="w-full lg:w-[750px]">
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
FEATURED
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 ">
|
||||
{/* === LEFT COLUMN === */}
|
||||
<div className=" w-full">
|
||||
<div className="relative w-full aspect-video mb-5">
|
||||
<Image
|
||||
src={data.leftMain.image}
|
||||
alt={data.leftMain.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<div className="absolute bottom-0.5 left-2 text-white">
|
||||
<h3 className=" font-semibold text-base mb-1">
|
||||
{data.leftMain.title}
|
||||
</h3>
|
||||
<p className=" text-xs mb-2 flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{data.rightMain.date}{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{data.leftList.map((item, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 flex gap-2 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{item.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === RIGHT COLUMN === */}
|
||||
<div className="w-full">
|
||||
<div className="relative w-full aspect-video mb-5">
|
||||
<Image
|
||||
src={data.rightMain.image}
|
||||
alt={data.rightMain.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 33vw"
|
||||
className="object-cover "
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<div className="absolute bottom-0.5 left-2 text-white">
|
||||
<h3 className=" font-semibold text-base mb-1">
|
||||
{data.leftMain.title}
|
||||
</h3>
|
||||
<p className=" text-xs mb-2 flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{data.rightMain.date}{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{data.rightList.map((item, i) => (
|
||||
<div key={i} className="flex gap-3">
|
||||
<div className="relative w-[120px] h-[86px] shrink-0">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover "
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 flex gap-2 items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{item.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
BasketBall
|
||||
</h2>
|
||||
<div className="w-full border bg-[#FAFAFA] h-[64px] flex justify-center items-center">
|
||||
<p className="text-center ">No Content Available</p>
|
||||
</div>
|
||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
Late News
|
||||
</h2>
|
||||
<div className="w-full border bg-[#FAFAFA] h-[64px] flex justify-center items-center">
|
||||
<p className="text-center ">No Content Available</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="w-full lg:w-[345px]">
|
||||
<div className="">
|
||||
<h2 className="text-sm border-b-2 border-gray-300 font-bold mb-4">
|
||||
RECOMMENDED
|
||||
</h2>
|
||||
|
||||
<div className="space-y-8">
|
||||
{popularPosts.map((post) => (
|
||||
<div key={post.id} className="space-y-3">
|
||||
{/* Title, Meta, Image in Flex */}
|
||||
<div
|
||||
className={`flex gap-4 ${
|
||||
post.image ? "flex-col md:flex-row" : "flex-col"
|
||||
}`}
|
||||
>
|
||||
{/* Text Content (Title + Meta) */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base sm:text-lg md:text-sm font-bold leading-tight">
|
||||
{post.title}
|
||||
</h3>
|
||||
<div className="text-xs mt-1.5 text-[#A0A0A0] space-x-2 flex items-center">
|
||||
{/* Clock Icon + Date */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 2a1 1 0 0 1 .993.883L13 7v4.586l2.707 2.707a1 1 0 0 1-1.32 1.497l-.094-.083l-3-3a1 1 0 0 1-.284-.576L11 12V7a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>{" "}
|
||||
{post.date}{" "}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>{" "}
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image on the right (md+) or bottom (sm) */}
|
||||
{post.image && (
|
||||
<div className="w-full md:w-1/3 relative">
|
||||
<div className="w-full aspect-[4/3] sm:w-[260px] sm:h-[157px] md:max-w-[320px] md:h-[220px] lg:max-w-[120px] lg:h-[87px] relative ml-auto">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Excerpt - always below */}
|
||||
<p className="text-[13px] text-[#3D4248] line-clamp-2 mb-3">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-[1111px] max-w-full h-[300px] overflow-hidden flex items-center mx-auto border my-6 rounded">
|
||||
<Image
|
||||
src="/kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Menu, Lock, Search } from "lucide-react";
|
||||
|
||||
const Navbar = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggleMenu = () => setIsOpen(!isOpen);
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-white shadow-md">
|
||||
<div className="max-w-7xl mx-auto py-2 px-0 md:px-4 flex flex-col md:flex-row justify-end items-start md:justify-between md:items-center gap-4 bg-[#308A2E]">
|
||||
<div className="flex items-center gap-4 w-full md:w-auto mx-3 md:mx-5">
|
||||
<div className="flex gap-3 text-xs mx-3 text-white">
|
||||
<Link href="/terpopuler" className="hover:text-yellow-400">
|
||||
About
|
||||
</Link>
|
||||
<Link href="/nusantara" className="hover:text-yellow-400">
|
||||
Advertise
|
||||
</Link>
|
||||
<Link href="/catatan" className="hover:text-yellow-400">
|
||||
Privacy & Policy
|
||||
</Link>
|
||||
<Link href="/inovasi" className="hover:text-yellow-400">
|
||||
Contact
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5 text-sm whitespace-nowrap text-white mx-5">
|
||||
<div className="hidden md:block min-w-fit whitespace-nowrap text-white text-xs">
|
||||
Kamis, Maret 27, 2025
|
||||
</div>
|
||||
<div className="flex gap-3 text-white text-lg">
|
||||
<Link href="#" aria-label="Facebook">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
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>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Twitter">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.91 20.889c8.302 0 12.845-6.885 12.845-12.845c0-.193 0-.387-.009-.58A9.2 9.2 0 0 0 23 5.121a9.2 9.2 0 0 1-2.597.713a4.54 4.54 0 0 0 1.99-2.5a9 9 0 0 1-2.87 1.091A4.5 4.5 0 0 0 16.23 3a4.52 4.52 0 0 0-4.516 4.516c0 .352.044.696.114 1.03a12.82 12.82 0 0 1-9.305-4.718a4.526 4.526 0 0 0 1.4 6.03a4.6 4.6 0 0 1-2.043-.563v.061a4.524 4.524 0 0 0 3.62 4.428a4.4 4.4 0 0 1-1.189.159q-.435 0-.845-.08a4.51 4.51 0 0 0 4.217 3.135a9.05 9.05 0 0 1-5.608 1.936A9 9 0 0 1 1 18.873a12.84 12.84 0 0 0 6.91 2.016"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Google" className="text-[#F5F5F5]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M7.796 14.333v-2.618h7.211c.066.382.12.763.12 1.265c0 4.364-2.923 7.462-7.33 7.462A7.63 7.63 0 0 1 .16 12.806a7.63 7.63 0 0 1 7.636-7.637c2.062 0 3.786.753 5.117 1.997L10.84 9.162c-.567-.546-1.56-1.178-3.044-1.178c-2.607 0-4.734 2.16-4.734 4.822s2.127 4.821 4.734 4.821c3.022 0 4.157-2.17 4.331-3.294zm13.27-2.6H23.2v2.134h-2.133V16h-2.134v-2.133H16.8v-2.134h2.133V9.6h2.134z"
|
||||
// clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Pinterest">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
id="akarIconsPinterestFill0"
|
||||
fill="#fff"
|
||||
d="M0 0h24v24H0z"
|
||||
/>
|
||||
</defs>
|
||||
<g fill="none">
|
||||
<g
|
||||
// clip-path="url(#akarIconsPinterestFill1)"
|
||||
>
|
||||
<g
|
||||
// clip-path="url(#akarIconsPinterestFill2)"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0 12c0 5.123 3.211 9.497 7.73 11.218c-.11-.937-.227-2.482.025-3.566c.217-.932 1.401-5.938 1.401-5.938s-.357-.715-.357-1.774c0-1.66.962-2.9 2.161-2.9c1.02 0 1.512.765 1.512 1.682c0 1.025-.653 2.557-.99 3.978c-.281 1.189.597 2.159 1.769 2.159c2.123 0 3.756-2.239 3.756-5.471c0-2.861-2.056-4.86-4.991-4.86c-3.398 0-5.393 2.549-5.393 5.184c0 1.027.395 2.127.889 2.726a.36.36 0 0 1 .083.343c-.091.378-.293 1.189-.332 1.355c-.053.218-.173.265-.4.159c-1.492-.694-2.424-2.875-2.424-4.627c0-3.769 2.737-7.229 7.892-7.229c4.144 0 7.365 2.953 7.365 6.899c0 4.117-2.595 7.431-6.199 7.431c-1.211 0-2.348-.63-2.738-1.373c0 0-.599 2.282-.744 2.84c-.282 1.084-1.064 2.456-1.549 3.235C9.584 23.815 10.77 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0S0 5.373 0 12"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="akarIconsPinterestFill1">
|
||||
<use href="#akarIconsPinterestFill0" />
|
||||
</clipPath>
|
||||
<clipPath id="akarIconsPinterestFill2">
|
||||
<use href="#akarIconsPinterestFill0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link href="#" aria-label="Vk">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
// fill-rule="evenodd"
|
||||
d="M23.45 5.948c.166-.546 0-.948-.795-.948H20.03c-.668 0-.976.347-1.143.73c0 0-1.335 3.196-3.226 5.272c-.612.602-.89.793-1.224.793c-.167 0-.418-.191-.418-.738V5.948c0-.656-.184-.948-.74-.948H9.151c-.417 0-.668.304-.668.593c0 .621.946.765 1.043 2.513v3.798c0 .833-.153.984-.487.984c-.89 0-3.055-3.211-4.34-6.885C4.45 5.288 4.198 5 3.527 5H.9c-.75 0-.9.347-.9.73c0 .682.89 4.07 4.145 8.551C6.315 17.341 9.37 19 12.153 19c1.669 0 1.875-.368 1.875-1.003v-2.313c0-.737.158-.884.687-.884c.39 0 1.057.192 2.615 1.667C19.11 18.216 19.403 19 20.405 19h2.625c.75 0 1.126-.368.91-1.096c-.238-.724-1.088-1.775-2.215-3.022c-.612-.71-1.53-1.475-1.809-1.858c-.389-.491-.278-.71 0-1.147c0 0 3.2-4.426 3.533-5.929"
|
||||
// clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="#" aria-label="Wifi">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 21q-1.05 0-1.775-.725T9.5 18.5t.725-1.775T12 16t1.775.725t.725 1.775t-.725 1.775T12 21m-5.65-5.65l-2.1-2.15q1.475-1.475 3.463-2.337T12 10t4.288.875t3.462 2.375l-2.1 2.1q-1.1-1.1-2.55-1.725T12 13t-3.1.625t-2.55 1.725M2.1 11.1L0 9q2.3-2.35 5.375-3.675T12 4t6.625 1.325T24 9l-2.1 2.1q-1.925-1.925-4.462-3.012T12 7T6.563 8.088T2.1 11.1"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/auth"
|
||||
className="hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Lock className="w-3 h-3" />
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#31942E] text-white">
|
||||
<div className="flex items-start justify-start md:justify-center md:items-center px-4 py-3 md:px-8 md:py-4">
|
||||
{/* Toggle Menu (Mobile Only) */}
|
||||
<button
|
||||
className="md:hidden flex items-center"
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<Menu className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
|
||||
{/* Navigation Links (Desktop Only) */}
|
||||
<div className="hidden md:flex items-center md:gap-x-32 font-semibold text-sm">
|
||||
<Link href="/" className="text-yellow-400 pl-4">
|
||||
BERANDA
|
||||
</Link>
|
||||
<Link href="/terpopuler" className="hover:text-yellow-400">
|
||||
PEMBANGUNAN
|
||||
</Link>
|
||||
<Link href="/nusantara" className="hover:text-yellow-400">
|
||||
KESEHATAN
|
||||
</Link>
|
||||
<Link href="/catatan" className="hover:text-yellow-400 pr-4">
|
||||
BERITA WARGA
|
||||
</Link>
|
||||
<Search className="w-5 h-5 hover:text-yellow-400 cursor-pointer ml-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="md:hidden px-4 pb-4 flex flex-col gap-2 font-semibold text-sm bg-[#31942E]">
|
||||
<Link href="/" className="text-yellow-400">
|
||||
BERANDA
|
||||
</Link>
|
||||
<Link href="/terpopuler" className="hover:text-yellow-400">
|
||||
TERPOPULER
|
||||
</Link>
|
||||
<Link href="/nusantara" className="hover:text-yellow-400">
|
||||
NUSANTARA
|
||||
</Link>
|
||||
<Link href="/catatan" className="hover:text-yellow-400">
|
||||
CATATAN NEGERI
|
||||
</Link>
|
||||
<Link href="/inovasi" className="hover:text-yellow-400">
|
||||
INOVASI
|
||||
</Link>
|
||||
<Link href="/jaga" className="hover:text-yellow-400">
|
||||
JAGA INDONESIA
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
import { close } from "@/config/swal";
|
||||
import { getListArticle } from "@/service/article";
|
||||
import { Clock } from "lucide-react";
|
||||
import { getPossibleInstrumentationHookFilenames } from "next/dist/build/utils";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type postsData = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
categoryName: string;
|
||||
createdAt: string;
|
||||
createdByName: string;
|
||||
thumbnailUrl: string;
|
||||
categories: {
|
||||
title: string;
|
||||
}[];
|
||||
files: {
|
||||
file_url: string;
|
||||
file_alt: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function Beranda() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [posts, setPosts] = useState<postsData[]>([]);
|
||||
const [showData, setShowData] = useState("5");
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||
const [startDateValue, setStartDateValue] = useState({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, startDateValue, selectedCategories]);
|
||||
|
||||
async function initState() {
|
||||
// loading();
|
||||
const req = {
|
||||
limit: showData,
|
||||
page,
|
||||
search,
|
||||
categorySlug: Array.from(selectedCategories).join(","),
|
||||
sort: "desc",
|
||||
sortBy: "created_at",
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await getListArticle(req);
|
||||
setPosts(res?.data?.data || []);
|
||||
setTotalPage(res?.data?.meta?.totalPage || 1);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<section className="max-w-7xl mx-auto px-10 py-5 bg-white">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-1 gap-2 bg-white border-b-2 pt-2 ">
|
||||
<h2 className="text-sm font-bold">Football</h2>
|
||||
<div className="flex flex-wrap gap-2 text-xs text-gray-600">
|
||||
<button className="hover:text-green-500">ALL</button>
|
||||
<button className="hover:text-green-500">Premier League</button>
|
||||
<button className="hover:text-green-500">The Presidents Cup</button>
|
||||
<button className="hover:text-green-500">Super Bowl</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 pt-4 ">
|
||||
{posts.slice(1, 5).map((posts, index) => (
|
||||
<div key={index} className="bg-white overflow-hidden">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={
|
||||
posts.thumbnailUrl ||
|
||||
posts?.files?.[0]?.file_url ||
|
||||
"/default-image.jpg"
|
||||
}
|
||||
alt={posts.title}
|
||||
width={500}
|
||||
height={300}
|
||||
className="w-full h-52 md:h-56 object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<span className="absolute top-1 left-1 bg-[#FFC600] text-black text-[11px] px-2 py-1 uppercase">
|
||||
{posts.categories?.[0]?.title}
|
||||
</span>
|
||||
<div className="p-3 md:p-2 absolute bottom-1 left-1 text-white">
|
||||
<h3 className="font-bold text-sm md:text-base leading-snug mb-1">
|
||||
{posts.title}
|
||||
</h3>
|
||||
|
||||
<div className="text-xs flex items-center gap-2">
|
||||
<Clock className="w-3 h-3 " />
|
||||
<span className=" ">
|
||||
{posts.createdByName} -{" "}
|
||||
{new Date(posts.createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative my-5 max-w-full h-[125px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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-10 w-full items-center rounded-md transition-colors cursor-pointer ${isActive ? "bg-slate-400 text-black" : "text-black hover:bg-slate-100"}`}
|
||||
>
|
||||
<motion.div layout className={`h-full ${open ? "w-10 grid place-content-center text-lg" : "flex-1 grid place-items-center text-lg"}`}>
|
||||
<Icon />
|
||||
</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">
|
||||
{title}
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
{!open && hovered && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: 8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 8 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||
className="absolute left-full ml-2 whitespace-nowrap rounded bg-slate-800 px-2 py-1 text-xs text-white shadow-md z-10"
|
||||
>
|
||||
{title}
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
{notifs && open && (
|
||||
<motion.span initial={{ scale: 0, opacity: 0 }} animate={{ opacity: 1, scale: 1 }} style={{ y: "-50%" }} transition={{ delay: 0.5 }} className="absolute right-2 top-1/2 size-4 rounded bg-indigo-500 text-xs text-white">
|
||||
{notifs}
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Option;
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
"use client";
|
||||
|
||||
import { Clock } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
const popularPosts = [
|
||||
{
|
||||
id: 1,
|
||||
image: "/presiden.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Presiden Prabowo Lantik 31 Duta Besar Baru",
|
||||
excerpt:
|
||||
"JAKARTA - Pada Senin (24/3) petang, Presiden Republik Indonesia, Prabowo Subianto, melantik 31 Duta Besar Luar Biasa dan Berkuasa Penuh...",
|
||||
author: "SALMA HASNA",
|
||||
date: "25 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: "/bkn.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"BKN Perkuat Keamanan Layanan Digital ASN dengan Multi-Factor Authentication",
|
||||
excerpt:
|
||||
"Jakarta – Badan Kepegawaian Negara (BKN) terus meningkatkan keamanan layanan digital bagi Aparatur Sipil Negara (ASN) dengan menerapkan sistem verifikasi...",
|
||||
author: "SALMA HASNA",
|
||||
date: "24 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: "/snbp.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Presiden Prabowo Lantik 31 Duta Besar Baru",
|
||||
excerpt:
|
||||
"JAKARTA - Pada Senin (24/3) petang, Presiden Republik Indonesia, Prabowo Subianto, melantik 31 Duta Besar Luar Biasa dan Berkuasa Penuh...",
|
||||
author: "SALMA HASNA",
|
||||
date: "25 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
image: "",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"BKN Perkuat Keamanan Layanan Digital ASN dengan Multi-Factor Authentication",
|
||||
excerpt:
|
||||
"Jakarta – Badan Kepegawaian Negara (BKN) terus meningkatkan keamanan layanan digital bagi Aparatur Sipil Negara (ASN) dengan menerapkan sistem verifikasi...",
|
||||
author: "SALMA HASNA",
|
||||
date: "24 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
image: "/bpi.jpg",
|
||||
category: "BERANDA",
|
||||
title: "Presiden Prabowo Lantik 31 Duta Besar Baru",
|
||||
excerpt:
|
||||
"JAKARTA - Pada Senin (24/3) petang, Presiden Republik Indonesia, Prabowo Subianto, melantik 31 Duta Besar Luar Biasa dan Berkuasa Penuh...",
|
||||
author: "SALMA HASNA",
|
||||
date: "25 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
image: "/pkp.jpg",
|
||||
category: "BERANDA",
|
||||
title:
|
||||
"BKN Perkuat Keamanan Layanan Digital ASN dengan Multi-Factor Authentication",
|
||||
excerpt:
|
||||
"Jakarta – Badan Kepegawaian Negara (BKN) terus meningkatkan keamanan layanan digital bagi Aparatur Sipil Negara (ASN) dengan menerapkan sistem verifikasi...",
|
||||
author: "SALMA HASNA",
|
||||
date: "24 MARET 2025",
|
||||
comments: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Popular() {
|
||||
return (
|
||||
<section className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="relative mb-10 h-[120px] sm:h-[160px] md:h-[188px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6 gap-4 border-b-2">
|
||||
<h2 className="text-lg sm:text-md font-serif font-semibold border-b-2 border-blue-700 inline-block pb-1">
|
||||
BERITA TERPOPULER
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-2 text-xs text-gray-600 uppercase font-medium">
|
||||
<button className="hover:text-blue-700">All</button>
|
||||
<button className="hover:text-blue-700">HUMAS.POLRI.GO.ID</button>
|
||||
<button className="hover:text-blue-700">TNI.MIL.ID</button>
|
||||
<button className="hover:text-blue-700">COVID19.GO.ID</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{popularPosts.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className={`flex gap-4 ${
|
||||
post.image ? "flex-col md:flex-row" : "flex-col"
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base sm:text-lg md:text-xl font-bold leading-tight">
|
||||
{post.title}
|
||||
</h3>
|
||||
<div className="text-xs mt-1.5 mb-2 text-gray-600 space-x-2 flex items-center">
|
||||
<span>
|
||||
BY{" "}
|
||||
<span className="text-blue-700 font-semibold">
|
||||
{post.author}
|
||||
</span>
|
||||
</span>
|
||||
<Clock className="w-3 h-3 text-blue-500" />
|
||||
<span> {post.date}</span>
|
||||
<div className="text-blue-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 1792 1536"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M896 128q-204 0-381.5 69.5T232.5 385T128 640q0 112 71.5 213.5T401 1029l87 50l-27 96q-24 91-70 172q152-63 275-171l43-38l57 6q69 8 130 8q204 0 381.5-69.5t282-187.5T1664 640t-104.5-255t-282-187.5T896 128m896 512q0 174-120 321.5t-326 233t-450 85.5q-70 0-145-8q-198 175-460 242q-49 14-114 22h-5q-15 0-27-10.5t-16-27.5v-1q-3-4-.5-12t2-10t4.5-9.5l6-9l7-8.5l8-9q7-8 31-34.5t34.5-38t31-39.5t32.5-51t27-59t26-76q-157-89-247.5-220T0 640q0-174 120-321.5t326-233T896 0t450 85.5t326 233T1792 640"
|
||||
/>
|
||||
</svg>{" "}
|
||||
</div>
|
||||
<span>{post.comments}</span>
|
||||
</div>
|
||||
<p className="text-[15px] text-[#5A515E] line-clamp-2 mb-3">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
<button className="border px-3 py-1.5 text-sm hover:shadow font-medium text-gray-800">
|
||||
READ MORE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{post.image && (
|
||||
<div className="w-full md:w-1/3 relative">
|
||||
<div className="w-full aspect-[4/3] sm:w-[260px] sm:h-[157px] md:max-w-[320px] md:h-[220px] lg:max-w-[360px] lg:h-[257px] relative ml-auto">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover rounded"
|
||||
/>
|
||||
<span className="absolute top-2 left-2 bg-red-700 text-white text-[10px] sm:text-[11px] px-2 py-[2px] uppercase font-bold">
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-2 justify-start">
|
||||
<button className="border px-3 py-1 text-xs hover:bg-gray-100 rounded-sm">
|
||||
‹ PREV
|
||||
</button>
|
||||
<button className="border px-3 py-1 text-xs hover:bg-gray-100 rounded-sm">
|
||||
NEXT ›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative my-6 h-[120px] sm:h-[160px] md:h-[188px] overflow-hidden flex items-center mx-auto border">
|
||||
<Image
|
||||
src="/image-kolom.png"
|
||||
alt="Berita Utama"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
"use client";
|
||||
|
||||
import React, { Dispatch, SetStateAction, useState } 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 Option from "./option";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
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: "Apps",
|
||||
items: [
|
||||
{
|
||||
title: "Artikel",
|
||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
||||
link: "/admin/article",
|
||||
},
|
||||
{
|
||||
title: "Kategori",
|
||||
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: "Advertise",
|
||||
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: "Master",
|
||||
items: [
|
||||
{
|
||||
title: "Master Static Page",
|
||||
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
|
||||
link: "/admin/static-page",
|
||||
},
|
||||
{
|
||||
title: "Master User",
|
||||
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
|
||||
link: "/admin/master-user",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const RetractingSidebar = ({
|
||||
sidebarData,
|
||||
updateSidebarData,
|
||||
}: RetractingSidebarProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* DESKTOP SIDEBAR */}
|
||||
<motion.nav
|
||||
layout
|
||||
className="hidden md:flex sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-1 flex-col justify-between"
|
||||
style={{
|
||||
width: sidebarData ? "160px" : "90px",
|
||||
}}
|
||||
>
|
||||
<SidebarContent
|
||||
open={sidebarData}
|
||||
pathname={pathname}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
</motion.nav>
|
||||
|
||||
{/* MOBILE SIDEBAR */}
|
||||
{sidebarData && (
|
||||
<motion.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween" }}
|
||||
className="fixed top-0 left-0 z-50 w-[250px] h-full bg-white p-4 flex flex-col md:hidden shadow-lg"
|
||||
>
|
||||
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
|
||||
✕
|
||||
</button> */}
|
||||
<SidebarContent
|
||||
open={true}
|
||||
pathname={pathname}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarContent = ({
|
||||
open,
|
||||
pathname,
|
||||
updateSidebarData,
|
||||
}: {
|
||||
open: boolean;
|
||||
pathname: string;
|
||||
updateSidebarData: (newData: boolean) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* BAGIAN ATAS */}
|
||||
<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={() => updateSidebarData(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="/mikul.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={() => updateSidebarData(false)}>
|
||||
✕
|
||||
</button>
|
||||
)} */}
|
||||
{open && (
|
||||
<button
|
||||
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
|
||||
onClick={() => updateSidebarData(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>
|
||||
<Link href={"/auth"}>
|
||||
<p className="underline">Logout</p>
|
||||
</Link>
|
||||
</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",
|
||||
}}
|
||||
>
|
||||
{/* BAGIAN ATAS */}
|
||||
<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,53 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// interface Props {
|
||||
// children: React.ReactNode;
|
||||
// }
|
||||
import React, { ReactNode } from "react";
|
||||
import { SidebarProvider } from "./sidebar-context";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { BurgerButtonIcon } from "../icons";
|
||||
import { RetractingSidebar } from "../landing-page/retracting-sidedar";
|
||||
|
||||
export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const updateSidebarData = (newData: boolean) => {
|
||||
setIsOpen(newData);
|
||||
};
|
||||
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setHasMounted(true);
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
if (!hasMounted) return null;
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="!h-screen flex items-center flex-row !overflow-y-hidden">
|
||||
<RetractingSidebar
|
||||
sidebarData={isOpen}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
<div className={`w-full h-full flex flex-col overflow-hidden`}>
|
||||
<div className="flex justify-between border-b-2 dark:border-b-2 items-center dark:bg-black dark:border-white">
|
||||
<Breadcrumbs />
|
||||
<button
|
||||
className="md:hidden items-center pr-4 justify-center h-10 w-10 flex z-50 text-zinc-700"
|
||||
onClick={() => updateSidebarData(true)}
|
||||
>
|
||||
<BurgerButtonIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
"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";
|
||||
|
||||
export const Breadcrumbs = () => {
|
||||
const [currentPage, setCurrentPage] = useState<React.Key>("");
|
||||
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(" ");
|
||||
});
|
||||
|
||||
console.log("pathname : ", pathnameTransformed);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[100px] w-full">
|
||||
<div className="px-4 md:px-8">
|
||||
<div className="flex flex-row justify-between items-center py-3">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div>
|
||||
<p className="text-2xl font-semibold mb-2">
|
||||
{pathnameTransformed[pathnameTransformed.length - 1]}
|
||||
</p>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{pathnameTransformed
|
||||
?.filter((item) => item !== "Admin")
|
||||
.map((item, index, array) => (
|
||||
<React.Fragment key={pathnameSplit[index]}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
onClick={() => handleAction(pathnameSplit[index])}
|
||||
className={
|
||||
pathnameSplit[index] === currentPage
|
||||
? "font-semibold"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{index < array.length - 1 && <BreadcrumbSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="lg:hidden">
|
||||
{!isOpen && (
|
||||
<button className="w-5 h-5 mb-3 text-zinc-400 dark:text-zinc-400 z-50 flex justify-center items-center" onClick={toggleSidebar}>
|
||||
<BurgerButtonIcon />
|
||||
</button>
|
||||
)}
|
||||
</div> */}
|
||||
<div className="hidden lg:block">
|
||||
{pathname.includes("dashboard") && <DashboardIcon size={50} />}
|
||||
{pathname.includes("article") && <ArticleIcon size={50} />}
|
||||
{pathname.includes("master-category") && (
|
||||
<MasterCategoryIcon size={50} />
|
||||
)}
|
||||
{pathname.includes("magazine") && <MagazineIcon size={50} />}
|
||||
{pathname.includes("static-page") && <StaticPageIcon size={50} />}
|
||||
{pathname.includes("master-user") && <MasterUsersIcon size={50} />}
|
||||
{pathname.includes("master-role") && <MasterRoleIcon size={50} />}
|
||||
{/* <FormLayoutIcon width={50} height={50} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
"use client";
|
||||
import { getStatisticMonthly } from "@/service/article";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type WeekData = {
|
||||
week: number;
|
||||
days: number[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
type RemainingDays = {
|
||||
days: number[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
function processMonthlyData(count: number[]): {
|
||||
weeks: WeekData[];
|
||||
remaining_days: RemainingDays;
|
||||
} {
|
||||
const weeks: WeekData[] = [];
|
||||
let weekIndex = 1;
|
||||
|
||||
for (let i = 0; i < count.length; i += 7) {
|
||||
const weekData = count.slice(i, i + 7);
|
||||
weeks.push({
|
||||
week: weekIndex,
|
||||
days: weekData,
|
||||
total: weekData.reduce((sum, day) => sum + day, 0),
|
||||
});
|
||||
weekIndex++;
|
||||
}
|
||||
|
||||
const remainingDays: RemainingDays = {
|
||||
days: count.length % 7 === 0 ? [] : count.slice(-count.length % 7),
|
||||
total: count.slice(-count.length % 7).reduce((sum, day) => sum + day, 0),
|
||||
};
|
||||
|
||||
return {
|
||||
weeks,
|
||||
remaining_days: remainingDays,
|
||||
};
|
||||
}
|
||||
|
||||
const ApexChartColumn = (props: {
|
||||
type: string;
|
||||
date: string;
|
||||
view: string[];
|
||||
}) => {
|
||||
const { date, type, view } = props;
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]);
|
||||
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
||||
const [seriesView, setSeriesView] = useState<number[]>([]);
|
||||
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
||||
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initFetch();
|
||||
}, [date, type, view]);
|
||||
|
||||
const initFetch = async () => {
|
||||
const splitDate = date.split(" ");
|
||||
|
||||
const res = await getStatisticMonthly(splitDate[1]);
|
||||
const data = res?.data?.data;
|
||||
const getDatas = data?.find(
|
||||
(a: any) =>
|
||||
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
|
||||
);
|
||||
if (getDatas) {
|
||||
const temp1 = processMonthlyData(getDatas?.comment);
|
||||
const temp2 = processMonthlyData(getDatas?.view);
|
||||
const temp3 = processMonthlyData(getDatas?.share);
|
||||
|
||||
if (type == "weekly") {
|
||||
setSeriesComment(
|
||||
temp1.weeks.map((list) => {
|
||||
return list.total;
|
||||
})
|
||||
);
|
||||
setSeriesView(
|
||||
temp2.weeks.map((list) => {
|
||||
return list.total;
|
||||
})
|
||||
);
|
||||
setSeriesShare(
|
||||
temp3.weeks.map((list) => {
|
||||
return list.total;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setSeriesComment(getDatas.comment);
|
||||
setSeriesView(getDatas.view);
|
||||
setSeriesShare(getDatas.share);
|
||||
}
|
||||
if (type === "weekly") {
|
||||
const category = [];
|
||||
for (let i = 1; i <= temp1.weeks.length; i++) {
|
||||
category.push(`Week ${i}`);
|
||||
}
|
||||
setCategories(category);
|
||||
}
|
||||
} else {
|
||||
setSeriesComment([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const temp = [
|
||||
{
|
||||
name: "Comment",
|
||||
data: view.includes("comment") ? seriesComment : [],
|
||||
},
|
||||
{
|
||||
name: "View",
|
||||
data: view.includes("view") ? seriesView : [],
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
data: view.includes("share") ? seriesShare : [],
|
||||
},
|
||||
];
|
||||
|
||||
console.log("temp", temp);
|
||||
|
||||
setSeries(temp);
|
||||
}, [view, seriesShare, seriesView, seriesComment]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div id="chart" className="h-full">
|
||||
<ReactApexChart
|
||||
options={{
|
||||
chart: {
|
||||
height: "100%",
|
||||
type: "area",
|
||||
},
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
categories: type == "weekly" ? categories : [],
|
||||
},
|
||||
}}
|
||||
series={series}
|
||||
type="area"
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
<div id="html-dist"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApexChartColumn;
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
DashboardCommentIcon,
|
||||
DashboardConnectIcon,
|
||||
DashboardShareIcon,
|
||||
DashboardSpeecIcon,
|
||||
DashboardUserIcon,
|
||||
} from "@/components/icons/dashboard-icon";
|
||||
import Cookies from "js-cookie";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Article } from "@/types/globals";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
getListArticle,
|
||||
getStatisticSummary,
|
||||
getTopArticles,
|
||||
getUserLevelDataStat,
|
||||
} from "@/service/article";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
convertDateFormat,
|
||||
convertDateFormatNoTime,
|
||||
textEllipsis,
|
||||
} from "@/utils/global";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
|
||||
import CustomPagination from "@/components/layout/custom-pagination";
|
||||
|
||||
type ArticleData = Article & {
|
||||
no: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
interface TopPages {
|
||||
id: number;
|
||||
no: number;
|
||||
title: string;
|
||||
viewCount: number;
|
||||
}
|
||||
|
||||
interface PostCount {
|
||||
userLevelId: number;
|
||||
no: number;
|
||||
userLevelName: string;
|
||||
totalArticle: number;
|
||||
}
|
||||
|
||||
export default function DashboardContainer() {
|
||||
const username = Cookies.get("username");
|
||||
const fullname = Cookies.get("ufne");
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [topPagesTotalPage, setTopPagesTotalPage] = useState(1);
|
||||
const [article, setArticle] = useState<ArticleData[]>([]);
|
||||
// const [analyticsView, setAnalyticView] = useState<string[]>(["comment", "view", "share"]);
|
||||
// const [startDateValue, setStartDateValue] = useState(parseDate(convertDateFormatNoTimeV2(new Date())));
|
||||
// const [postContentDate, setPostContentDate] = useState({
|
||||
// startDate: parseDate(convertDateFormatNoTimeV2(new Date(new Date().setDate(new Date().getDate() - 7)))),
|
||||
// endDate: parseDate(convertDateFormatNoTimeV2(new Date())),
|
||||
// });
|
||||
|
||||
const [startDateValue, setStartDateValue] = useState(new Date());
|
||||
const [analyticsView, setAnalyticView] = useState<string[]>([]);
|
||||
const options = [
|
||||
{ label: "Comment", value: "comment" },
|
||||
{ label: "View", value: "view" },
|
||||
{ label: "Share", value: "share" },
|
||||
];
|
||||
const handleChange = (value: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setAnalyticView([...analyticsView, value]);
|
||||
} else {
|
||||
setAnalyticView(analyticsView.filter((v) => v !== value));
|
||||
}
|
||||
};
|
||||
const [postContentDate, setPostContentDate] = useState({
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
endDate: new Date(),
|
||||
});
|
||||
|
||||
const [typeDate, setTypeDate] = useState("monthly");
|
||||
const [summary, setSummary] = useState<any>();
|
||||
|
||||
const [topPages, setTopPages] = useState<TopPages[]>([]);
|
||||
const [postCount, setPostCount] = useState<PostCount[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSummary();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page]);
|
||||
|
||||
async function initState() {
|
||||
const req = {
|
||||
limit: "4",
|
||||
page: page,
|
||||
search: "",
|
||||
};
|
||||
const res = await getListArticle(req);
|
||||
setArticle(res.data?.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
async function fetchSummary() {
|
||||
const res = await getStatisticSummary();
|
||||
setSummary(res?.data?.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopPages();
|
||||
}, [page]);
|
||||
|
||||
async function fetchTopPages() {
|
||||
const req = {
|
||||
limit: "10",
|
||||
page: page,
|
||||
search: "",
|
||||
};
|
||||
const res = await getTopArticles(req);
|
||||
setTopPages(getTableNumber(10, res.data?.data));
|
||||
setTopPagesTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPostCount();
|
||||
}, [postContentDate]);
|
||||
async function fetchPostCount() {
|
||||
const getDate = (data: any) => {
|
||||
return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
|
||||
data.day < 10 ? `0${data.day}` : data.day
|
||||
}`;
|
||||
};
|
||||
const res = await getUserLevelDataStat(
|
||||
getDate(postContentDate.startDate),
|
||||
getDate(postContentDate.endDate)
|
||||
);
|
||||
setPostCount(getTableNumber(10, res?.data?.data));
|
||||
}
|
||||
|
||||
const getTableNumber = (limit: number, data: any) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
};
|
||||
|
||||
const getMonthYear = (date: any) => {
|
||||
return date.month + " " + date.year;
|
||||
};
|
||||
const getMonthYearName = (date: any) => {
|
||||
const newDate = new Date(date);
|
||||
|
||||
const months = [
|
||||
"Januari",
|
||||
"Februari",
|
||||
"Maret",
|
||||
"April",
|
||||
"Mei",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"Agustus",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Desember",
|
||||
];
|
||||
const year = newDate.getFullYear();
|
||||
|
||||
const month = months[newDate.getMonth()];
|
||||
return month + " " + year;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-2 lg:px-4 py-4 flex justify-center">
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
{/* <div className="flex flex-row justify-between border-b-2">
|
||||
<div className="flex flex-col gap-1 py-2">
|
||||
<h1 className="font-bold text-[25px]">Dashboard</h1>
|
||||
<p className="text-[14px]">Dashboard</p>
|
||||
</div>
|
||||
<span className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div> */}
|
||||
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center">
|
||||
<div className="px-4 lg:px-8 py-4 justify-between w-full lg:w-[35%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col rounded-lg">
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-bold text-xl ">{fullname}</p>
|
||||
<p>{username}</p>
|
||||
</div>
|
||||
<DashboardUserIcon size={78} />
|
||||
</div>
|
||||
<div className="flex flex-row gap-5">
|
||||
<p className="text-lg font-semibold">
|
||||
{summary?.totalToday} Post{" "}
|
||||
<span className="text-sm font-normal">Hari ini</span>
|
||||
</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{summary?.totalThisWeek} Post{" "}
|
||||
<span className="text-sm font-normal">Minggu ini </span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:w-[20%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
|
||||
<div className="h-1/2 flex items-center justify-center">
|
||||
<DashboardSpeecIcon />
|
||||
</div>
|
||||
<div className="">Total post</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalAll}</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
|
||||
<div className="h-1/2 flex items-center justify-center">
|
||||
<DashboardConnectIcon />
|
||||
</div>
|
||||
<div className="">Total views</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalViews}</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
|
||||
<div className="h-1/2 flex items-center justify-center">
|
||||
<DashboardShareIcon />
|
||||
</div>
|
||||
<div className="">Total share</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalShares}</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
|
||||
<div className="h-1/2 flex items-center justify-center">
|
||||
<DashboardCommentIcon size={50} />
|
||||
</div>
|
||||
<div className="">Total comment</div>
|
||||
<div className="font-semibold text-lg">
|
||||
{summary?.totalComments}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center ">
|
||||
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[55%] p-6 flex flex-col text-xs lg:text-sm">
|
||||
<div className="flex justify-between mb-4 items-center">
|
||||
<p className="font-semibold">
|
||||
Rekapitulasi Post Berita Polda/Polres Pada Website
|
||||
</p>
|
||||
<div className="w-[220px] flex flex-row gap-2 justify-between font-semibold">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<a className="cursor-pointer text-sm font-medium">
|
||||
{convertDateFormatNoTime(postContentDate.startDate)}
|
||||
</a>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-auto p-0 bg-transparent border-none shadow-none">
|
||||
<DatePicker
|
||||
selected={postContentDate.startDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setPostContentDate((prev) => ({
|
||||
...prev,
|
||||
startDate: date,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
maxDate={postContentDate.endDate}
|
||||
inline
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
-
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<a className="cursor-pointer ">
|
||||
{convertDateFormatNoTime(postContentDate.endDate)}
|
||||
</a>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="bg-transparent">
|
||||
<DatePicker
|
||||
selected={postContentDate.endDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setPostContentDate((prev) => ({
|
||||
...prev,
|
||||
endDateDate: date,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
maxDate={postContentDate.endDate}
|
||||
inline
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row border-b-1 gap-1 py-1">
|
||||
<div className="w-[5%]">NO</div>
|
||||
<div className="w-[50%] lg:w-[70%]">POLDA/POLRES</div>
|
||||
<div className="w-[45%] lg:w-[25%] text-right">
|
||||
JUMLAH POST BERITA
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 lg:h-[500px] overflow-y-auto">
|
||||
{postCount?.map((list) => (
|
||||
<div
|
||||
key={list.userLevelId}
|
||||
className="flex flex-row border-b-1 gap-1 py-1"
|
||||
>
|
||||
<div className="w-[5%]">{list?.no}</div>
|
||||
<div className="w-[85%]">{list?.userLevelName}</div>
|
||||
<div
|
||||
className={`w-[10%] text-center ${
|
||||
list?.totalArticle === 0 && "bg-red-600 text-white"
|
||||
}`}
|
||||
>
|
||||
{list?.totalArticle}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-sm">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<p>Recent Article</p>
|
||||
<Link href="/admin/article/create">
|
||||
<Button className="border border-black">Buat Article</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{article?.map((list: any) => (
|
||||
<div
|
||||
key={list?.id}
|
||||
className="flex flex-row gap-2 items-center border-b-2 py-2"
|
||||
>
|
||||
<Image
|
||||
alt="thumbnail"
|
||||
src={list?.thumbnailUrl || `/no-image.jpg`}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="h-[70px] w-[70px] object-cover rounded-lg"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>{textEllipsis(list?.title, 78)}</p>
|
||||
<p className="text-xs">
|
||||
{convertDateFormat(list?.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious onClick={handlePrevious} />
|
||||
</PaginationItem>
|
||||
|
||||
{Array.from({ length: totalPage }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink isActive={page === i + 1} onClick={() => setPage(i + 1)}>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext onClick={handleNext} />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center ">
|
||||
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[55%] p-6 flex flex-col">
|
||||
<div className="flex justify-between mb-3">
|
||||
<div className="font-semibold flex flex-col">
|
||||
Analytics
|
||||
<div className="font-normal text-xs text-gray-600 flex flex-row gap-2">
|
||||
{/* Checkbox Group */}
|
||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
|
||||
{options.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={option.value}
|
||||
checked={analyticsView.includes(option.value)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(option.value, Boolean(checked))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={option.value}>{option.label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<Button
|
||||
variant={typeDate === "monthly" ? "default" : "outline"}
|
||||
onClick={() => setTypeDate("monthly")}
|
||||
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
|
||||
>
|
||||
Bulanan
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setTypeDate("weekly")}
|
||||
variant={typeDate === "weekly" ? "default" : "outline"}
|
||||
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
|
||||
>
|
||||
Mingguan
|
||||
</Button>
|
||||
|
||||
<div className="w-[140px]">
|
||||
{/* <Datepicker
|
||||
value={startDateValue}
|
||||
displayFormat="DD/MM/YYYY"
|
||||
asSingle={true}
|
||||
useRange={false}
|
||||
onChange={(e: any) => setStartDateValue(e)}
|
||||
inputClassName="z-50 w-full text-xs lg:text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-sm lg:rounded-lg h-[30px] lg:h-[40px] text-gray-600 dark:text-gray-300"
|
||||
/> */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className="w-full">
|
||||
{getMonthYearName(startDateValue)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 bg-transparent">
|
||||
<Calendar
|
||||
selected={startDateValue}
|
||||
onSelect={(day) => {
|
||||
if (day) setStartDateValue(day);
|
||||
}}
|
||||
mode="single"
|
||||
className="rounded-md border"
|
||||
/>{" "}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full h-full">
|
||||
<div className="w-full h-[30vh] lg:h-full text-black">
|
||||
<ApexChartColumn
|
||||
type={typeDate}
|
||||
date={startDateValue.toLocaleString("default", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
view={analyticsView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-xs lg:text-sm">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<p>Top Pages</p>
|
||||
</div>
|
||||
<div className="flex flex-row border-b-1">
|
||||
<div className="w-[5%]">No</div>
|
||||
<div className="w-[85%]">Title</div>
|
||||
<div className="w-[10%] text-center">Visits</div>
|
||||
</div>
|
||||
{topPages?.map((list) => (
|
||||
<div key={list.id} className="flex flex-row border-b-1">
|
||||
<div className="w-[5%]">{list?.no}</div>
|
||||
<div className="w-[85%]">{list?.title}</div>
|
||||
<div className="w-[10%] text-center">{list?.viewCount}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious onClick={handlePrevious} />
|
||||
</PaginationItem>
|
||||
|
||||
{Array.from({ length: totalPage }).map((_, i) => (
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink isActive={page === i + 1} onClick={() => setPage(i + 1)}>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext onClick={handleNext} />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination> */}
|
||||
<CustomPagination
|
||||
totalPage={topPagesTotalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
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 { useRouter } from "next/navigation";
|
||||
import dynamic from "next/dynamic";
|
||||
import DOMPurify from "dompurify";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { createCustomStaticPage } from "@/service/static-page-service";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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 StaticPageBuilder() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
|
||||
const formOptions = {
|
||||
resolver: zodResolver(formSchema),
|
||||
};
|
||||
type UserSettingSchema = z.infer<typeof formSchema>;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
|
||||
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: sanitizedContent }} /> */}
|
||||
<div dangerouslySetInnerHTML={{ __html: textArea.value }} />
|
||||
</Card>
|
||||
);
|
||||
}, [content]);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
const request = {
|
||||
title: values.title,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
htmlBody: values.htmlBody,
|
||||
};
|
||||
loading();
|
||||
const res = await createCustomStaticPage(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-1">
|
||||
<Label htmlFor="title">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-1">
|
||||
<Label htmlFor="slug">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="w-full space-y-1">
|
||||
<Label htmlFor="description">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 } }) => (
|
||||
// <Textarea
|
||||
// variant="bordered"
|
||||
// label=""
|
||||
// labelPlacement="outside"
|
||||
// placeholder=""
|
||||
// className="max-h-[80vh]"
|
||||
// classNames={{
|
||||
// mainWrapper: "h-[80vh] overflow-hidden",
|
||||
// innerWrapper: "h-[80vh] overflow-hidden",
|
||||
// input: "min-h-full",
|
||||
// inputWrapper: "h-full",
|
||||
// }}
|
||||
// value={value}
|
||||
// onChange={onChange}
|
||||
// disableAutosize={false}
|
||||
// />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,606 @@
|
|||
"use client";
|
||||
import {
|
||||
CloudUploadIcon,
|
||||
CreateIconIon,
|
||||
DeleteIcon,
|
||||
DotsYIcon,
|
||||
SearchIcon,
|
||||
TimesIcon,
|
||||
} from "@/components/icons";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { close, error, loading, success } from "@/config/swal";
|
||||
import { Article } from "@/types/globals";
|
||||
import Link from "next/link";
|
||||
import { Fragment, Key, useCallback, useEffect, useState } from "react";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import Image from "next/image";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import useDisclosure from "@/components/useDisclosure";
|
||||
import {
|
||||
createAdvertiseById,
|
||||
createMediaFileAdvertise,
|
||||
deleteAdvertise,
|
||||
editAdvertise,
|
||||
editAdvertiseIsActive,
|
||||
getAdvertise,
|
||||
} from "@/service/advertisement";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "@/components/layout/custom-pagination";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Judul", uid: "title" },
|
||||
{ name: "Deskripsi", uid: "description" },
|
||||
{ name: "Penempatan", uid: "placement" },
|
||||
{ name: "Link", uid: "redirectLink" },
|
||||
{ name: "Aktif", uid: "isActive" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
const createArticleSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string().min(2, {
|
||||
message: "Judul harus diisi",
|
||||
}),
|
||||
url: z.string().min(1, {
|
||||
message: "Url harus diisi",
|
||||
}),
|
||||
description: z.string().min(2, {
|
||||
message: "Deskripsi harus diisi",
|
||||
}),
|
||||
file: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function AdvertiseTable(props: { triggerRefresh: boolean }) {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [article, setArticle] = useState<any[]>([]);
|
||||
const [showData, setShowData] = useState("10");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [placement, setPlacement] = useState("banner");
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const formOptions = {
|
||||
resolver: zodResolver(createArticleSchema),
|
||||
defaultValues: { title: "", description: "", url: "", file: "" },
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFiles(acceptedFiles.map((file) => Object.assign(file)));
|
||||
},
|
||||
maxFiles: 1,
|
||||
accept: {
|
||||
"image/*": [],
|
||||
},
|
||||
});
|
||||
type UserSettingSchema = z.infer<typeof createArticleSchema>;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, props.triggerRefresh, refresh]);
|
||||
|
||||
const handleRemoveFile = (file: File) => {
|
||||
const uploadedFiles = files;
|
||||
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
|
||||
setFiles([...filtered]);
|
||||
};
|
||||
|
||||
async function initState() {
|
||||
const req = {
|
||||
limit: showData,
|
||||
page: page,
|
||||
search: search,
|
||||
|
||||
sort: "desc",
|
||||
sortBy: "created_at",
|
||||
};
|
||||
const res = await getAdvertise(req);
|
||||
getTableNumber(parseInt(showData), res.data?.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
const getTableNumber = (limit: number, data: Article[]) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setArticle(newData);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: any) {
|
||||
loading();
|
||||
const resDelete = await deleteAdvertise(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
success("Berhasil Hapus");
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
|
||||
const handleDelete = (id: any) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
|
||||
loading();
|
||||
const formData = {
|
||||
id: Number(values.id),
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
placement: placement,
|
||||
redirectLink: values.url,
|
||||
};
|
||||
const res = await editAdvertise(formData);
|
||||
if (res?.error) {
|
||||
error(res?.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
const formFiles = new FormData();
|
||||
formFiles.append("file", files[0]);
|
||||
const resFile = await createMediaFileAdvertise(
|
||||
Number(values.id),
|
||||
formFiles
|
||||
);
|
||||
}
|
||||
|
||||
close();
|
||||
MySwal.fire({
|
||||
title: "Sukses",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openModal = async (id: number) => {
|
||||
const res = await createAdvertiseById(Number(id));
|
||||
const data = res?.data?.data;
|
||||
setValue("id", String(data?.id));
|
||||
setValue("title", data?.title);
|
||||
setValue("description", data?.description);
|
||||
setValue("url", data?.redirectLink);
|
||||
setPlacement(data?.placement);
|
||||
// setValue("file", data?.thumbnailUrl);
|
||||
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const handleAdvertise = async (e: boolean, id: number) => {
|
||||
const res = await editAdvertiseIsActive({ id, isActive: e });
|
||||
if (res?.error) {
|
||||
error(res?.message);
|
||||
return false;
|
||||
}
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
const renderCell = useCallback(
|
||||
(advertise: any, columnKey: Key) => {
|
||||
const cellValue = advertise[columnKey as keyof any];
|
||||
|
||||
switch (columnKey) {
|
||||
case "redirectLink":
|
||||
return cellValue.includes("https") ? (
|
||||
<Link
|
||||
href={cellValue}
|
||||
target="_blank"
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
{cellValue}
|
||||
</Link>
|
||||
) : (
|
||||
<p> {cellValue}</p>
|
||||
);
|
||||
case "placement":
|
||||
return <p className="capitalize">{cellValue}</p>;
|
||||
case "isActive":
|
||||
return (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Switch
|
||||
checked={advertise?.isPublish}
|
||||
onCheckedChange={(e) => handleAdvertise(e, advertise?.id)}
|
||||
/>
|
||||
{advertise?.isPublish ? "Ya" : "Tidak"}
|
||||
</div>
|
||||
);
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-start items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<DotsYIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="lg:min-w-[150px] bg-black text-white shadow border">
|
||||
{/*
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/admin/advertise/detail/${article.id}`}>
|
||||
<EyeIconMdi className="inline mr-2 mb-1" />
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
*/}
|
||||
|
||||
<DropdownMenuItem onClick={() => openModal(advertise.id)}>
|
||||
<CreateIconIon className="inline mr-2 mb-1" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={() => handleDelete(advertise.id)}>
|
||||
<DeleteIcon
|
||||
color="red"
|
||||
size={18}
|
||||
className="inline ml-1 mr-2 mb-1"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
},
|
||||
[article, props.triggerRefresh, refresh]
|
||||
);
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
|
||||
async function doneTyping() {
|
||||
initState();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="py-3">
|
||||
<div className="flex flex-col items-start rounded-2xl gap-3">
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
||||
<p className="font-semibold text-sm">Pencarian</p>
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-base text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
aria-label="Search"
|
||||
type="text"
|
||||
className="pl-10 text-sm bg-muted"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
||||
<p className="font-semibold text-sm">Data</p>
|
||||
<Select
|
||||
value={showData}
|
||||
onValueChange={(value) =>
|
||||
value === "" ? "" : setShowData(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Table className="rounded-3xl min-h-[50px] bg-white dark:bg-black border text-black dark:text-white">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.uid}
|
||||
className="bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
||||
>
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{article.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-4"
|
||||
>
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
article.map((item: any) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.uid}>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<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
|
||||
type="text"
|
||||
id="title"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full border border-gray-300 dark:border-gray-400 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 border-gray-300 dark:border-gray-400 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
|
||||
type="text"
|
||||
id="url"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full border border-gray-300 dark:border-gray-400 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" className="text-sm">
|
||||
Banner
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="jumbotron" id="jumbotron" />
|
||||
<label htmlFor="jumbotron" className="text-sm">
|
||||
Jumbotron
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="file"
|
||||
render={({ field: { value } }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm mt-3">Thumbnail</p>
|
||||
|
||||
{files.length < 1 && value === "" && (
|
||||
<Fragment>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className="w-full text-center border-dashed border border-gray-300 rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUploadIcon />
|
||||
<h4 className="text-2xl font-medium mb-1 mt-3 text-gray-700">
|
||||
Tarik file disini atau klik untuk upload.
|
||||
</h4>
|
||||
<div className="text-xs text-gray-500">
|
||||
( Upload file dengan format .jpg, .jpeg, atau .png.
|
||||
Ukuran maksimal 100mb.)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{value !== "" && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<Image
|
||||
src={String(value)}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
width={480}
|
||||
height={480}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setValue("file", "")}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<img
|
||||
src={URL.createObjectURL(files[0])}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(files[0])}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="mt-4 flex justify-end gap-2">
|
||||
<Button type="submit">Simpan</Button>
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Tutup
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
"use client";
|
||||
import {
|
||||
BannerIcon,
|
||||
CopyIcon,
|
||||
CreateIconIon,
|
||||
DeleteIcon,
|
||||
DotsYIcon,
|
||||
EyeIconMdi,
|
||||
SearchIcon,
|
||||
} from "@/components/icons";
|
||||
import { close, error, loading, success, successToast } from "@/config/swal";
|
||||
import { Article } from "@/types/globals";
|
||||
import { convertDateFormat } from "@/utils/global";
|
||||
import Link from "next/link";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Cookies from "js-cookie";
|
||||
import {
|
||||
deleteArticle,
|
||||
getArticleByCategory,
|
||||
getListArticle,
|
||||
updateIsBannerArticle,
|
||||
} from "@/service/article";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "../layout/custom-pagination";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Judul", uid: "title" },
|
||||
{ name: "Banner", uid: "isBanner" },
|
||||
{ name: "Kategori", uid: "category" },
|
||||
{ name: "Tanggal Unggah", uid: "createdAt" },
|
||||
{ name: "Kreator", uid: "createdByName" },
|
||||
{ name: "Status", uid: "isPublish" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
const columnsOtherRole = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Judul", uid: "title" },
|
||||
{ name: "Kategori", uid: "category" },
|
||||
{ name: "Tanggal Unggah", uid: "createdAt" },
|
||||
{ name: "Kreator", uid: "createdByName" },
|
||||
{ name: "Status", uid: "isPublish" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
// interface Category {
|
||||
// id: number;
|
||||
// title: string;
|
||||
// }
|
||||
|
||||
export default function ArticleTable() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const username = Cookies.get("username");
|
||||
const userId = Cookies.get("uie");
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [article, setArticle] = useState<any[]>([]);
|
||||
const [showData, setShowData] = useState("10");
|
||||
const [search, setSearch] = useState("");
|
||||
const [categories, setCategories] = useState<any>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
||||
const [startDateValue, setStartDateValue] = useState({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, startDateValue, selectedCategories]);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
async function getCategories() {
|
||||
const res = await getArticleByCategory();
|
||||
const data = res?.data?.data;
|
||||
setCategories(data);
|
||||
}
|
||||
|
||||
async function initState() {
|
||||
loading();
|
||||
const req = {
|
||||
limit: showData,
|
||||
page: page,
|
||||
search: search,
|
||||
// startDate:
|
||||
// startDateValue.startDate === null ? "" : startDateValue.startDate,
|
||||
// endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
|
||||
categorySlug: Array.from(selectedCategories).join(","),
|
||||
sort: "desc",
|
||||
sortBy: "created_at",
|
||||
};
|
||||
const res = await getListArticle(req);
|
||||
await getTableNumber(parseInt(showData), res.data?.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
close();
|
||||
}
|
||||
|
||||
const getTableNumber = async (limit: number, data: Article[]) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setArticle(newData);
|
||||
} else {
|
||||
setArticle([]);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: any) {
|
||||
// loading();
|
||||
const resDelete = await deleteArticle(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
success("Berhasil Hapus");
|
||||
initState();
|
||||
}
|
||||
|
||||
const handleDelete = (id: any) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleBanner = async (id: number, status: boolean) => {
|
||||
const res = await updateIsBannerArticle(id, status);
|
||||
if (res?.error) {
|
||||
error(res?.message);
|
||||
return false;
|
||||
}
|
||||
initState();
|
||||
};
|
||||
|
||||
const copyUrlArticle = async (id: number, slug: string) => {
|
||||
const url =
|
||||
`${window.location.protocol}//${window.location.host}` +
|
||||
"/news/detail/" +
|
||||
`${id}-${slug}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
successToast("Success", "Article Copy to Clipboard");
|
||||
setTimeout(() => {}, 1500);
|
||||
} catch (err) {
|
||||
("Failed to copy!");
|
||||
}
|
||||
};
|
||||
|
||||
const renderCell = useCallback(
|
||||
(article: any, columnKey: Key) => {
|
||||
const cellValue = article[columnKey as keyof any];
|
||||
|
||||
switch (columnKey) {
|
||||
case "isPublish":
|
||||
return (
|
||||
// <Chip
|
||||
// className="capitalize "
|
||||
// color={statusColorMap[article.status]}
|
||||
// size="lg"
|
||||
// variant="flat"
|
||||
// >
|
||||
// <div className="flex flex-row items-center gap-2 justify-center">
|
||||
// {article.status}
|
||||
// </div>
|
||||
// </Chip>
|
||||
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
||||
);
|
||||
case "isBanner":
|
||||
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
||||
case "createdAt":
|
||||
return <p>{convertDateFormat(article.createdAt)}</p>;
|
||||
case "category":
|
||||
return (
|
||||
<p>
|
||||
{article?.categories?.map((list: any) => list.title).join(", ") +
|
||||
" "}
|
||||
</p>
|
||||
);
|
||||
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<DotsYIcon className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuItem
|
||||
onClick={() => copyUrlArticle(article.id, article.slug)}
|
||||
>
|
||||
<CopyIcon className="mr-2 h-4 w-4" />
|
||||
Copy Url Article
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/article/detail/${article.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<EyeIconMdi className="mr-2 h-4 w-4" />
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{(username === "admin-mabes" ||
|
||||
Number(userId) === article.createdById) && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/article/edit/${article.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<CreateIconIon className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{username === "admin-mabes" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleBanner(article.id, !article.isBanner)
|
||||
}
|
||||
>
|
||||
<BannerIcon className="mr-2 h-4 w-4" />
|
||||
{article.isBanner
|
||||
? "Hapus dari Banner"
|
||||
: "Jadikan Banner"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{(username === "admin-mabes" ||
|
||||
Number(userId) === article.createdById) && (
|
||||
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
|
||||
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
},
|
||||
[article, page]
|
||||
);
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
|
||||
async function doneTyping() {
|
||||
setPage(1);
|
||||
initState();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="py-3">
|
||||
<div className="flex flex-col items-start rounded-2xl gap-3">
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
||||
<p className="font-semibold text-sm">Pencarian</p>
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-4 w-4 pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Cari..."
|
||||
className="pl-9 text-sm bg-muted"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
||||
<p className="font-semibold text-sm">Data</p>
|
||||
<Select
|
||||
value={showData}
|
||||
onValueChange={(value) => setShowData(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full text-sm border">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[230px]">
|
||||
<p className="font-semibold text-sm">Kategori</p>
|
||||
<Select
|
||||
value={selectedCategories}
|
||||
onValueChange={(value) => setSelectedCategories(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full text-sm border">
|
||||
<SelectValue placeholder="Kategori" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories
|
||||
?.filter((category: any) => category.slug != null)
|
||||
.map((category: any) => (
|
||||
<SelectItem key={category.slug} value={category.slug}>
|
||||
{category.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
||||
<p className="font-semibold text-sm">Tanggal</p>
|
||||
<Datepicker
|
||||
value={startDateValue}
|
||||
displayFormat="DD/MM/YYYY"
|
||||
onChange={(e: any) => setStartDateValue(e)}
|
||||
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="w-full overflow-x-hidden">
|
||||
<div className="w-full mx-auto overflow-x-hidden">
|
||||
<Table className="w-full table-fixed border text-sm">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{(username === "admin-mabes"
|
||||
? columns
|
||||
: columnsOtherRole
|
||||
).map((column) => (
|
||||
<TableHead
|
||||
key={column.uid}
|
||||
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
||||
>
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{article.length > 0 ? (
|
||||
article.map((item: any) => (
|
||||
<TableRow key={item.id}>
|
||||
{(username === "admin-mabes"
|
||||
? columns
|
||||
: columnsOtherRole
|
||||
).map((column) => (
|
||||
<TableCell
|
||||
key={column.uid}
|
||||
className="truncate text-black dark:text-white max-w-[200px]"
|
||||
>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-4"
|
||||
>
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
"use client";
|
||||
import {
|
||||
CreateIconIon,
|
||||
DeleteIcon,
|
||||
DotsYIcon,
|
||||
EyeIconMdi,
|
||||
SearchIcon,
|
||||
} from "@/components/icons";
|
||||
import { close, error, loading, success } from "@/config/swal";
|
||||
import { getArticleByCategory } from "@/service/article";
|
||||
import { deleteMagazine, getListMagazine } from "@/service/magazine";
|
||||
import { Article } from "@/types/globals";
|
||||
import { convertDateFormat } from "@/utils/global";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "@/components/layout/custom-pagination";
|
||||
import { Chip, ChipProps } from "@/components/ui/chip";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Judul", uid: "title" },
|
||||
// { name: "Kategori", uid: "categoryName" },
|
||||
{ name: "Tanggal Unggah", uid: "createdAt" },
|
||||
{ name: "Kreator", uid: "createdByName" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
type ArticleData = Article & {
|
||||
no: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export default function MagazineTable() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [article, setArticle] = useState<ArticleData[]>([]);
|
||||
const [showData, setShowData] = useState("10");
|
||||
const [search, setSearch] = useState("");
|
||||
const [, setCategoies] = useState<any>([]);
|
||||
const [startDateValue] = useState({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, startDateValue]);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
async function getCategories() {
|
||||
const res = await getArticleByCategory();
|
||||
const data = res?.data?.data;
|
||||
console.log("datass", res?.data?.data);
|
||||
setCategoies(data);
|
||||
}
|
||||
|
||||
async function initState() {
|
||||
const req = {
|
||||
limit: showData,
|
||||
page: page,
|
||||
search: search,
|
||||
startDate:
|
||||
startDateValue.startDate === null ? "" : startDateValue.startDate,
|
||||
endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
|
||||
};
|
||||
const res = await getListMagazine(req);
|
||||
getTableNumber(parseInt(showData), res.data?.data);
|
||||
console.log("res.data?.data magz", res.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
const getTableNumber = (limit: number, data: Article[]) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
console.log("daata", data);
|
||||
setArticle(newData);
|
||||
} else {
|
||||
setArticle([]);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: any) {
|
||||
loading();
|
||||
const resDelete = await deleteMagazine(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
success("Berhasil Hapus");
|
||||
initState();
|
||||
}
|
||||
|
||||
const handleDelete = (id: any) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderCell = useCallback((article: ArticleData, columnKey: Key) => {
|
||||
const cellValue = article[columnKey as keyof ArticleData];
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "primary",
|
||||
cancel: "danger",
|
||||
pending: "success",
|
||||
};
|
||||
|
||||
switch (columnKey) {
|
||||
case "status":
|
||||
return (
|
||||
<Chip
|
||||
className="capitalize "
|
||||
color={statusColorMap[article.status]}
|
||||
size="lg"
|
||||
variant="flat"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2 justify-center">
|
||||
{cellValue}
|
||||
</div>
|
||||
</Chip>
|
||||
);
|
||||
case "createdAt":
|
||||
return <p>{convertDateFormat(article.createdAt)}</p>;
|
||||
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-start items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<DotsYIcon className="text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="bg-black text-white border shadow-lg">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/magazine/detail/${article.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<EyeIconMdi className="inline mr-2 mb-1" />
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/magazine/edit/${article.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<CreateIconIon className="inline mr-2 mb-1" />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(article.id)}
|
||||
className="text-red-500"
|
||||
>
|
||||
<DeleteIcon
|
||||
width={20}
|
||||
height={16}
|
||||
className="inline mr-2 mb-1"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
|
||||
async function doneTyping() {
|
||||
initState();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="py-3 w-full">
|
||||
<div className="flex flex-col items-start rounded-2xl gap-3">
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
||||
<p className="font-semibold text-sm">Pencarian</p>
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-base text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="pl-10 text-sm bg-muted"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
||||
<p className="font-semibold text-sm">Data</p>
|
||||
<Select
|
||||
onValueChange={(value) => setShowData(value)}
|
||||
value={showData}
|
||||
>
|
||||
<SelectTrigger className="w-full border">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* <div className="flex flex-col gap-1 w-[230px]">
|
||||
<p className="font-semibold text-sm">Kategori</p>
|
||||
<Select
|
||||
label=""
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder="Select"
|
||||
selectionMode="multiple"
|
||||
selectedKeys={[selectedCategories]}
|
||||
className="w-full"
|
||||
classNames={{ trigger: "border-1" }}
|
||||
onChange={(e) => {
|
||||
e.target.value === ""
|
||||
? ""
|
||||
: setSelectedCategories(e.target.value);
|
||||
}}
|
||||
>
|
||||
{categories?.map((category: any) => (
|
||||
<SelectItem key={category?.id} value={category?.id}>
|
||||
{category?.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div> */}
|
||||
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
||||
<p className="font-semibold text-sm">Tanggal</p>
|
||||
<Datepicker
|
||||
value={startDateValue}
|
||||
displayFormat="DD/MM/YYYY"
|
||||
onChange={(e: any) => setStartDateValue(e)}
|
||||
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="rounded-3xl border overflow-hidden w-full">
|
||||
<Table>
|
||||
<thead className="bg-white dark:bg-black text-black dark:text-white border-b">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.uid} className="text-md">
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<TableBody>
|
||||
{article.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-4"
|
||||
>
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
article.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.uid}>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,712 @@
|
|||
"use client";
|
||||
import {
|
||||
CloudUploadIcon,
|
||||
CreateIconIon,
|
||||
DeleteIcon,
|
||||
DotsYIcon,
|
||||
EyeIconMdi,
|
||||
SearchIcon,
|
||||
} from "@/components/icons";
|
||||
import { Article } from "@/types/globals";
|
||||
import { convertDateFormat } from "@/utils/global";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import ReactSelect from "react-select";
|
||||
import makeAnimated from "react-select/animated";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { close, error, loading, success } from "@/config/swal";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Image from "next/image";
|
||||
import useDisclosure from "@/components/useDisclosure";
|
||||
import { getArticleByCategory, getCategoryPagination } from "@/service/article";
|
||||
import {
|
||||
deleteCategory,
|
||||
getCategoryById,
|
||||
updateCategory,
|
||||
uploadCategoryThumbnail,
|
||||
} from "@/service/master-categories";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "@/components/layout/custom-pagination";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Kategori", uid: "title" },
|
||||
{ name: "Deskripsi", uid: "description" },
|
||||
{ name: "Tag Terkait", uid: "tags" },
|
||||
{ name: "Dibuat ", uid: "createdAt" },
|
||||
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
interface CategoryType {
|
||||
id: number;
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
type ArticleData = Article & {
|
||||
no: number;
|
||||
createdAt: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
// const categorySchema = z.object({
|
||||
// id: z.number(),
|
||||
// label: z.string(),
|
||||
// value: z.number(),
|
||||
// });
|
||||
|
||||
const createArticleSchema = z.object({
|
||||
id: z.string().min(1, {
|
||||
message: "Id harus valid",
|
||||
}),
|
||||
title: z.string().min(2, {
|
||||
message: "Judul harus diisi",
|
||||
}),
|
||||
description: z.string().min(2, {
|
||||
message: "Deskripsi harus diisi",
|
||||
}),
|
||||
tags: z.array(z.string()),
|
||||
|
||||
file: z.string(),
|
||||
});
|
||||
|
||||
export default function CategoriesTable(props: { triggerRefresh: boolean }) {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
|
||||
const animatedComponents = makeAnimated();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [categories, setCategories] = useState<ArticleData[]>([]);
|
||||
const [showData, setShowData] = useState("10");
|
||||
const [search, setSearch] = useState("");
|
||||
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isDetail, setIsDetail] = useState(false);
|
||||
const [tag, setTag] = useState("");
|
||||
const formOptions = {
|
||||
resolver: zodResolver(createArticleSchema),
|
||||
defaultValues: { title: "", description: "", category: [], tags: [] },
|
||||
};
|
||||
const [selectedParent, setSelectedParent] = useState<any>();
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFiles(acceptedFiles.map((file) => Object.assign(file)));
|
||||
},
|
||||
maxFiles: 1,
|
||||
accept: {
|
||||
"image/*": [],
|
||||
},
|
||||
});
|
||||
type UserSettingSchema = z.infer<typeof createArticleSchema>;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, props.triggerRefresh]);
|
||||
|
||||
async function initState() {
|
||||
const req = {
|
||||
limit: showData,
|
||||
page: page,
|
||||
search: search,
|
||||
};
|
||||
const res = await getCategoryPagination(req);
|
||||
getTableNumber(parseInt(showData), res.data?.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
const getTableNumber = (limit: number, data: Article[]) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setCategories(newData);
|
||||
} else {
|
||||
setCategories([]);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: number) {
|
||||
// loading();
|
||||
const resDelete = await deleteCategory(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
success("Berhasil Hapus");
|
||||
initState();
|
||||
}
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 openModal = async (id: number | string, detail: boolean) => {
|
||||
setIsDetail(detail);
|
||||
const res = await getCategoryById(Number(id));
|
||||
const data = res?.data?.data;
|
||||
setValue("id", String(data?.id));
|
||||
setValue("title", data?.title);
|
||||
setValue("description", data?.description);
|
||||
setValue("tags", data?.tags);
|
||||
setValue("file", data?.thumbnailUrl);
|
||||
findParent(data?.parentId);
|
||||
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const findParent = (parent: number | undefined) => {
|
||||
const finded = listCategory?.find((a: any) => a.id === parent);
|
||||
if (finded) {
|
||||
setSelectedParent(finded);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCell = useCallback(
|
||||
(category: ArticleData, columnKey: Key) => {
|
||||
const cellValue = category[columnKey as keyof ArticleData];
|
||||
// const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
// active: "primary",
|
||||
// cancel: "danger",
|
||||
// pending: "success",
|
||||
// };
|
||||
|
||||
// const findRelated = (parent: number | string) => {
|
||||
// const filter = listCategory?.filter((a) => a.id == parent);
|
||||
// return filter[0]?.label;
|
||||
// };
|
||||
|
||||
switch (columnKey) {
|
||||
case "tags":
|
||||
return (
|
||||
<div className="flex flex-row gap-1">
|
||||
{category.tags
|
||||
? category.tags.map((value) => value).join(", ")
|
||||
: "-"}
|
||||
</div>
|
||||
);
|
||||
case "createdAt":
|
||||
return <p>{convertDateFormat(category.createdAt)}</p>;
|
||||
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-star items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<DotsYIcon className="text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="lg:min-w-[150px] bg-black text-white shadow border">
|
||||
<DropdownMenuItem
|
||||
onClick={() => openModal(category.id, true)}
|
||||
>
|
||||
<EyeIconMdi className="inline mr-2 mb-1" />
|
||||
Detail
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => openModal(category.id, false)}
|
||||
>
|
||||
<CreateIconIon className="inline mr-2 mb-1" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(category.id)}>
|
||||
<DeleteIcon
|
||||
color="red"
|
||||
size={20}
|
||||
className="inline mr-3 mb-1"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
},
|
||||
[listCategory]
|
||||
);
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
|
||||
async function doneTyping() {
|
||||
initState();
|
||||
}
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
|
||||
loading();
|
||||
const formData = {
|
||||
id: Number(values.id),
|
||||
title: values.title,
|
||||
statusId: 1,
|
||||
parentId: selectedParent ? selectedParent.id : 0,
|
||||
tags: values.tags.join(","),
|
||||
description: values.description,
|
||||
};
|
||||
|
||||
const response = await updateCategory(values.id, formData);
|
||||
|
||||
if (response?.error) {
|
||||
error(response.message);
|
||||
return false;
|
||||
}
|
||||
if (files?.length > 0) {
|
||||
const formFiles = new FormData();
|
||||
|
||||
formFiles.append("files", files[0]);
|
||||
const resFile = await uploadCategoryThumbnail(values.id, formFiles);
|
||||
if (resFile?.error) {
|
||||
error(resFile.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setFiles([]);
|
||||
close();
|
||||
initState();
|
||||
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="py-3 w-full">
|
||||
<div className="flex flex-col items-start rounded-2xl gap-3 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
||||
<p className="font-semibold text-sm">Pencarian</p>
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Cari..."
|
||||
aria-label="Search"
|
||||
className="pl-10 text-sm bg-muted"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
||||
<p className="font-semibold text-sm">Data</p>
|
||||
<Select
|
||||
value={showData}
|
||||
onValueChange={(value) =>
|
||||
value === "" ? "" : setShowData(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Table
|
||||
aria-label="micro issue table"
|
||||
className="rounded-3xl"
|
||||
classNames={{
|
||||
th: "bg-white dark:bg-black text-black dark:text-white border-b-1 text-md",
|
||||
base: "bg-white dark:bg-black border",
|
||||
wrapper: "min-h-[50px] bg-transpararent text-black dark:text-white ",
|
||||
}}
|
||||
>
|
||||
<TableHeader columns={columns}>{(column) => <TableColumn key={column.uid}>{column.name}</TableColumn>}</TableHeader>
|
||||
<TableBody items={categories} emptyContent={"No data to display."} loadingContent={<Spinner label="Loading..." />}>
|
||||
{(item) => <TableRow key={item.id}>{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}</TableRow>}
|
||||
</TableBody>
|
||||
</Table> */}
|
||||
<div className="rounded-3xl border bg-white dark:bg-black text-black dark:text-white w-full">
|
||||
<Table className="min-h-[50px] w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.uid}
|
||||
className="text-md border-b bg-white dark:bg-black text-black dark:text-white"
|
||||
>
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categories.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-4"
|
||||
>
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
categories.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.uid}>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Kategori Baru</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[70vh] pr-4">
|
||||
<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"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={isDetail}
|
||||
className="rounded-lg border dark:border-gray-400"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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="rounded-lg border dark:border-gray-400"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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="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 } }) => (
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="tags"
|
||||
type="text"
|
||||
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();
|
||||
}
|
||||
}
|
||||
}}
|
||||
readOnly={isDetail}
|
||||
className="rounded-lg border dark:border-gray-400 h-[45px]"
|
||||
/>
|
||||
<div className="absolute top-2 left-3 flex gap-1">
|
||||
{value?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-blue-100 text-blue-700 px-2 py-1 text-xs rounded flex items-center gap-1"
|
||||
>
|
||||
{item}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const filtered = value.filter(
|
||||
(tag) => tag !== item
|
||||
);
|
||||
if (filtered.length === 0) {
|
||||
setError("tags", {
|
||||
type: "manual",
|
||||
message: "Tags tidak boleh kosong",
|
||||
});
|
||||
} else {
|
||||
clearErrors("tags");
|
||||
setValue(
|
||||
"tags",
|
||||
filtered as [string, ...string[]]
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isDetail ? (
|
||||
<img
|
||||
src={getValues("file")}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
width={480}
|
||||
height={480}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name="file"
|
||||
render={({ field: { value } }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm mt-3">Thumbnail</p>
|
||||
{files.length < 1 && value === "" && (
|
||||
<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">
|
||||
Tarik file disini atau klik untuk upload.
|
||||
</h4>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
( Upload file .jpg, .jpeg, .png. Maks 100mb )
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{value !== "" && (
|
||||
<div className="flex gap-2">
|
||||
<img
|
||||
src={value}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setValue("file", "")}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
<Image
|
||||
src={URL.createObjectURL(files[0])}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveFile(files[0])}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2 mt-4">
|
||||
{!isDetail && <Button type="submit">Simpan</Button>}
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Tutup
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
"use client";
|
||||
import { CreateIconIon, DeleteIcon, DotsYIcon } from "@/components/icons";
|
||||
import { close, error } from "@/config/swal";
|
||||
import { deleteMasterUser, listMasterUsers } from "@/service/master-user";
|
||||
import { MasterUser } from "@/types/globals";
|
||||
import Link from "next/link";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "../layout/custom-pagination";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Username", uid: "username" },
|
||||
{ name: "Fullname", uid: "fullname" },
|
||||
{ name: "Email", uid: "email" },
|
||||
{ name: "Identity Type", uid: "identityType" },
|
||||
{ name: "Identity Number", uid: "identityNumber" },
|
||||
// { name: "Users", uid: "users" },
|
||||
// { name: "Status", uid: "status" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
export default function MasterUserTable() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const [user, setUser] = useState<MasterUser[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page]);
|
||||
|
||||
async function initState() {
|
||||
const res = await listMasterUsers({ page: page, limit: 10 });
|
||||
getTableNumber(10, res?.data?.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
const getTableNumber = (limit: number, data?: any) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setUser(newData);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: string) {
|
||||
// loading();
|
||||
const resDelete = await deleteMasterUser(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
successSubmit();
|
||||
}
|
||||
|
||||
const handleDelete = (id: any) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function successSubmit() {
|
||||
MySwal.fire({
|
||||
title: "Sukses",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
initState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const renderCell = useCallback((user: MasterUser, columnKey: Key) => {
|
||||
const cellValue = user[columnKey as keyof MasterUser];
|
||||
// const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
// active: "primary",
|
||||
// cancel: "danger",
|
||||
// pending: "success",
|
||||
// };
|
||||
|
||||
switch (columnKey) {
|
||||
case "id":
|
||||
return <div>{user.id}</div>;
|
||||
|
||||
case "status":
|
||||
return (
|
||||
<div></div>
|
||||
// <Chip
|
||||
// className="capitalize "
|
||||
// // color={statusColorMap[user.status]}
|
||||
// size="lg"
|
||||
// variant="flat"
|
||||
// >
|
||||
// <div className="flex flex-row items-center gap-2 justify-center">{cellValue}</div>
|
||||
// </Chip>
|
||||
);
|
||||
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-start items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<DotsYIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/master-user/edit/${user.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<CreateIconIon className="inline mr-2 mb-1" />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="text-red-600"
|
||||
>
|
||||
<DeleteIcon
|
||||
width={20}
|
||||
height={16}
|
||||
className="inline mr-2 mb-1"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-3 my-5">
|
||||
<div className="flex flex-col items-center rounded-2xl">
|
||||
<Table className="rounded-2xl text-black dark:text-white bg-white dark:bg-black min-h-[50px]">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.uid}
|
||||
className="bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
||||
>
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{user.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="text-center">
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
user.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.uid}>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
"use client";
|
||||
import {
|
||||
CreateIconIon,
|
||||
DeleteIcon,
|
||||
DotsYIcon,
|
||||
SearchIcon,
|
||||
} from "@/components/icons";
|
||||
import { close, error, success } from "@/config/swal";
|
||||
import { deleteArticle } from "@/service/article";
|
||||
import { getCustomStaticPage } from "@/service/static-page-service";
|
||||
import { Article } from "@/types/globals";
|
||||
import Link from "next/link";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "../ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import CustomPagination from "../layout/custom-pagination";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Judul", uid: "title" },
|
||||
{ name: "Slug", uid: "slug" },
|
||||
{ name: "Deskripsi", uid: "description" },
|
||||
{ name: "Aksi", uid: "actions" },
|
||||
];
|
||||
|
||||
type ArticleData = Article & {
|
||||
no: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export default function StaticPageTable() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [article, setArticle] = useState<ArticleData[]>([]);
|
||||
const [showData, setShowData] = useState("10");
|
||||
const [search, setSearch] = useState("");
|
||||
const [startDateValue] = useState({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initState();
|
||||
}, [page, showData, startDateValue]);
|
||||
|
||||
async function initState() {
|
||||
const req = {
|
||||
limit: showData,
|
||||
page: page,
|
||||
search: search,
|
||||
};
|
||||
const res = await getCustomStaticPage(req);
|
||||
getTableNumber(parseInt(showData), res.data?.data);
|
||||
console.log("res.data?.data", res.data);
|
||||
setTotalPage(res?.data?.meta?.totalPage);
|
||||
}
|
||||
|
||||
const getTableNumber = (limit: number, data: Article[]) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
console.log("daata", data);
|
||||
setArticle(newData);
|
||||
}
|
||||
};
|
||||
|
||||
async function doDelete(id: string) {
|
||||
// loading();
|
||||
const resDelete = await deleteArticle(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
close();
|
||||
success("Success Deleted");
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
doDelete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// function successSubmit() {
|
||||
// MySwal.fire({
|
||||
// title: "Sukses",
|
||||
// icon: "success",
|
||||
// confirmButtonColor: "#3085d6",
|
||||
// confirmButtonText: "OK",
|
||||
// }).then((result) => {
|
||||
// if (result.isConfirmed) {
|
||||
// // initStete();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
const renderCell = useCallback((article: ArticleData, columnKey: Key) => {
|
||||
const cellValue = article[columnKey as keyof ArticleData];
|
||||
|
||||
switch (columnKey) {
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-start items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="p-0 h-auto w-auto"
|
||||
>
|
||||
<DotsYIcon className="text-black dark:text-white" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-black text-white border">
|
||||
{/* <DropdownMenuItem>
|
||||
<Link href={`/admin/static-page/detail/${article.id}`} className="flex items-center">
|
||||
<EyeIconMdi className="inline mr-2 mb-1" />
|
||||
Detail
|
||||
</Link>
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/admin/static-page/edit/${article.id}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<CreateIconIon className="inline mr-2 mb-1" size={20} />
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(article.id)}
|
||||
className="flex items-center text-red-500"
|
||||
>
|
||||
<DeleteIcon
|
||||
color="red"
|
||||
width={20}
|
||||
height={16}
|
||||
className="inline mr-2 mb-1"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
let typingTimer: NodeJS.Timeout;
|
||||
const doneTypingInterval = 1500;
|
||||
|
||||
const handleKeyUp = () => {
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||
};
|
||||
|
||||
const handleKeyDown = () => {
|
||||
clearTimeout(typingTimer);
|
||||
};
|
||||
|
||||
async function doneTyping() {
|
||||
initState();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="py-3">
|
||||
<div className="flex flex-col items-start rounded-2xl gap-3">
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
||||
<Label className="font-semibold text-sm">Pencarian</Label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="text-base text-muted-foreground" />
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
aria-label="Pencarian..."
|
||||
placeholder="Pencarian..."
|
||||
className="pl-10 text-sm bg-muted"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
||||
<Label className="font-semibold text-sm">Data</Label>
|
||||
<Select
|
||||
value={showData}
|
||||
onValueChange={(value) =>
|
||||
value === "" ? "" : setShowData(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-col gap-1 w-full lg:w-[340px]">
|
||||
<p className="font-semibold text-sm">Tanggal</p>
|
||||
<Datepicker
|
||||
value={startDateValue}
|
||||
displayFormat="DD/MM/YYYY"
|
||||
onChange={(e: any) => setStartDateValue(e)}
|
||||
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
<Table className="rounded-3xl overflow-hidden border border-gray-200 dark:border-gray-800 shadow-sm bg-white dark:bg-black text-black dark:text-white min-h-[50px]">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-white dark:bg-black border-b dark:border-gray-800">
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.uid}
|
||||
className="text-left font-semibold text-sm text-black dark:text-white px-4 py-3"
|
||||
>
|
||||
{column.name}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{article.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="text-center py-4 text-sm"
|
||||
>
|
||||
No data to display.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
article.map((item) => (
|
||||
<TableRow key={item.id} className="transition-colors">
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.uid}
|
||||
className="px-4 py-3 text-sm border-none"
|
||||
>
|
||||
{renderCell(item, column.uid)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="my-2 w-full flex justify-center">
|
||||
{/* <Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/> */}
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormLayoutIcon } from "../icons";
|
||||
import { BreadcrumbItem, BreadcrumbList } from "../ui/breadcrumb";
|
||||
|
||||
export const Breadcrumb = () => {
|
||||
const [, setCurrentPage] = useState<React.Key>("");
|
||||
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(" ");
|
||||
});
|
||||
|
||||
console.log("pathname : ", pathnameTransformed);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
|
||||
}, [pathnameSplit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className=" h-[100px] gap-0 grid rounded-lg border-small m-1 md:m-0 md:ml-4">
|
||||
<div className="mx-2 md:px-6 ">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="pt-1 w-full">
|
||||
<p className="text-2xl font-semibold mb-2">
|
||||
{pathnameTransformed[pathnameTransformed.length - 1]}
|
||||
</p>
|
||||
<BreadcrumbList>
|
||||
{pathnameTransformed?.map((item, index) => (
|
||||
<BreadcrumbItem key={pathnameSplit[index]}>
|
||||
{item}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<FormLayoutIcon width={50} height={50} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
import * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={
|
||||
{
|
||||
// IconLeft: ({ className, ...props }) => (
|
||||
// <ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
// ),
|
||||
// IconRight: ({ className, ...props }) => (
|
||||
// <ChevronRight className={cn("size-4", className)} {...props} />
|
||||
// ),
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export type ChipColor = "default" | "primary" | "success" | "danger";
|
||||
|
||||
export interface ChipProps {
|
||||
children: React.ReactNode;
|
||||
color?: ChipColor;
|
||||
className?: string;
|
||||
size?: string;
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
const colorMap: Record<ChipColor, string> = {
|
||||
default: "bg-gray-200 text-gray-800",
|
||||
primary: "bg-blue-100 text-blue-800",
|
||||
success: "bg-green-100 text-green-800",
|
||||
danger: "bg-red-100 text-red-800",
|
||||
};
|
||||
|
||||
export const Chip: React.FC<ChipProps> = ({ children, color = "default", className }) => {
|
||||
return <span className={clsx("inline-flex items-center px-3 py-1 rounded-full text-sm font-medium", colorMap[color], className)}>{children}</span>;
|
||||
};
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { useState, useCallback } from "react";
|
||||
|
||||
function useDisclosure(initial = false) {
|
||||
const [isOpen, setIsOpen] = useState(initial);
|
||||
|
||||
const onOpen = useCallback(() => setIsOpen(true), []);
|
||||
const onClose = useCallback(() => setIsOpen(false), []);
|
||||
const onOpenChange = useCallback(() => setIsOpen((prev) => !prev), []);
|
||||
|
||||
return { isOpen, onOpen, onClose, onOpenChange };
|
||||
}
|
||||
|
||||
export default useDisclosure;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export const useSidebar = () => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsOpen(prev => !prev);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
toggleSidebar,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const Toast = MySwal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast: any) => {
|
||||
toast.addEventListener("mouseenter", Swal.stopTimer);
|
||||
toast.addEventListener("mouseleave", Swal.resumeTimer);
|
||||
},
|
||||
});
|
||||
|
||||
export function loading(msg?: any) {
|
||||
let timerInterval: any;
|
||||
MySwal.fire({
|
||||
title: msg || "Loading...",
|
||||
allowOutsideClick: false,
|
||||
timerProgressBar: true,
|
||||
didOpen: () => {
|
||||
MySwal.showLoading();
|
||||
timerInterval = setInterval(() => {}, 100);
|
||||
},
|
||||
willClose: () => {
|
||||
clearInterval(timerInterval);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function error(msg?: any) {
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Failed...",
|
||||
text: msg || "Unknown Error",
|
||||
customClass: {
|
||||
popup: "custom-popup",
|
||||
confirmButton: "custom-button",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function successRouter(redirect: string, router?: any) {
|
||||
MySwal.fire({
|
||||
title: "Success!",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#6642f5",
|
||||
confirmButtonText: "Ok",
|
||||
allowOutsideClick: false,
|
||||
}).then((result: any) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push(redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function success(title: string) {
|
||||
Swal.fire({
|
||||
title: "Success!",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#6642f5",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
|
||||
export function close() {
|
||||
MySwal.close();
|
||||
}
|
||||
|
||||
export function warning(text: string, redirect: string, router?: any) {
|
||||
MySwal.fire({
|
||||
title: text,
|
||||
icon: "warning",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result: any) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push(redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function successToast(title: string, text: string) {
|
||||
Toast.fire({
|
||||
icon: "success",
|
||||
title: title,
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
domains: ["kontenhumas.com"],
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue