Initial commit

This commit is contained in:
Anang Yusman 2025-10-06 14:21:08 +08:00
commit b9bb86d15c
1023 changed files with 931602 additions and 0 deletions

41
.gitignore vendored Normal file
View File

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

36
README.md Normal file
View File

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

View File

@ -0,0 +1,273 @@
"use client";
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 {
createAdvertise,
createMediaFileAdvertise,
} 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";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import useDisclosure from "@/components/useDisclosure";
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 payload = {
Title: values.title,
Description: values.description,
Placement: placement,
RedirectLink: values.url,
};
const res = await createAdvertise(payload);
if (res?.error) {
error(res?.message);
return false;
}
const idNow = res?.data?.data?.id;
if (files.length > 0 && idNow) {
const formFiles = new FormData();
formFiles.append("file", files[0]);
const resFile = await createMediaFileAdvertise(idNow, formFiles);
if (resFile?.error) {
error(resFile?.message);
return false;
}
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
setRefresh(!refresh);
}
});
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<Button
size="default"
className="bg-[#F07C00] text-white w-full lg:w-fit"
onClick={onOpen}
>
Buat Baru
<AddIcon />
</Button>
<AdvertiseTable triggerRefresh={refresh} />
</div>
</div>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Advertise</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-1">
<p className="text-sm">Judul</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
id="title"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm">{errors.title?.message}</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<Textarea
id="description"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm">
{errors.description?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Link</p>
<Controller
control={control}
name="url"
render={({ field: { onChange, value } }) => (
<Input
id="url"
value={value}
onChange={onChange}
className="w-full border rounded-lg"
/>
)}
/>
{errors?.url && (
<p className="text-red-400 text-sm">{errors.url?.message}</p>
)}
</div>
<p className="text-sm mt-3">Penempatan</p>
<RadioGroup
value={placement}
onValueChange={setPlacement}
className="flex flex-row gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="banner" id="banner" />
<Label htmlFor="banner">Banner</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="jumbotron" id="jumbotron" />
<Label htmlFor="jumbotron">Jumbotron</Label>
</div>
</RadioGroup>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>
{files.length < 1 && (
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex items-center flex-col">
<CloudUploadIcon />
<h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className="text-xs text-muted-foreground">
(Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
maksimal 100mb.)
</div>
</div>
</div>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<img
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
variant="outline"
size="icon"
onClick={() => handleRemoveFile(files[0])}
>
<TimesIcon />
</Button>
</div>
)}
</div>
<DialogFooter className="pt-4">
<Button type="submit">Simpan</Button>
<Button variant="ghost" onClick={onClose}>
Tutup
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

19
app/detail/[id]/page.tsx Normal file
View File

@ -0,0 +1,19 @@
import DetailContent from "@/components/details/details-content";
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
import Image from "next/image";
export default function Home() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar />
<div className="flex-1">
<DetailContent />
</div>
<Footer />
</div>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

122
app/globals.css Normal file
View File

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

34
app/layout.tsx Normal file
View File

@ -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: "Create Next App",
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>
);
}

21
app/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import Footer from "@/components/landing-page/footer";
import Beranda from "@/components/landing-page/headers";
import MoreNews from "@/components/landing-page/more-news";
import Navbar from "@/components/landing-page/navbar";
import News from "@/components/landing-page/news";
import TravelNews from "@/components/landing-page/travel-news";
export default function Home() {
return (
<div className="flex min-h-screen flex-col font-[family-name:var(--font-geist-sans)] bg-white">
<Navbar />
<div className="flex-1">
<Beranda />
</div>
<News />
<TravelNews />
<MoreNews />
<Footer />
</div>
);
}

21
components.json Normal file
View File

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

View File

@ -0,0 +1,514 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import Link from "next/link";
import { getArticleById, getListArticle } from "@/service/article";
import { close, loading } from "@/config/swal";
import { useParams } from "next/navigation";
import { CommentIcon } from "../icons/sidebar-icon";
type TabKey = "trending" | "comments" | "latest";
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;
}[];
};
interface CategoryType {
id: number;
label: string;
value: number;
}
export default function DetailContent() {
const params = useParams();
const id = params?.id;
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [articleDetail, setArticleDetail] = useState<any>(null);
const [showData, setShowData] = useState("5");
const [search, setSearch] = useState("");
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
const [detailfiles, setDetailFiles] = useState<any>([]);
const [mainImage, setMainImage] = useState(0);
const [thumbnail, setThumbnail] = useState("-");
const [diseId, setDiseId] = useState(0);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null
);
const [tabArticles, setTabArticles] = useState<Article[]>([]);
const [activeTab, setActiveTab] = useState<TabKey>("trending");
const tabs: { id: TabKey; label: string }[] = [
{ id: "trending", label: "Trending" },
{ id: "comments", label: "Comments" },
{ id: "latest", label: "Latest" },
];
useEffect(() => {
initState();
}, [page, showData, startDateValue, selectedCategories]);
async function initState() {
// loading();
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} finally {
// close();
}
}
const content: Record<
TabKey,
{ image: string; title: string; date: string }[]
> = {
trending: [
{
image: "/thumb1.png",
title:
"#StopBullyDiSekolah: Peran Positif Media Sosial dalam Mengatasi Bullying",
date: "22 FEBRUARI 2024",
},
{
image: "/thumb2.png",
title:
"Polri Gelar Lomba Orasi Unjuk Rasa dalam Rangka Hari HAM Sedunia Berhadiah Total Lebih dari Rp 150 juta!",
date: "29 NOVEMBER 2021",
},
{
image: "/thumb3.png",
title: "Tingkatkan Ibadah Sambut #RamadhanPenuhDamai",
date: "7 MARET 2024",
},
{
image: "/thumb4.png",
title:
"Exploring the Charm of Papuas Traditional Clothing: A Captivating and Meaningful Cultural Heritage",
date: "1 AGUSTUS 2024",
},
],
comments: [
{
image: "/thumb-comment.png",
title: "Pengunjung Komentar Positif tentang Fitur Baru",
date: "3 JUNI 2024",
},
],
latest: [
{
image: "/thumb-latest.png",
title: "Update Terbaru dari Redaksi Hari Ini",
date: "2 JULI 2025",
},
],
};
useEffect(() => {
initStateData();
}, [listCategory]);
async function initStateData() {
loading();
const res = await getArticleById(id);
const data = res.data?.data;
setThumbnail(data?.thumbnailUrl);
setDiseId(data?.aiArticleId);
setDetailFiles(data?.files);
setArticleDetail(data); // <-- Add this
close();
}
return (
<>
<div className="bg-white grid grid-cols-1 md:grid-cols-3 gap-6 px-8 py-8">
<div className="md:col-span-2">
<p className="text-sm text-gray-500 mb-2">Home {">"}Detail</p>
<h1 className="text-3xl md:text-4xl font-bold text-[#1a1a1a] leading-tight mb-4">
{articleDetail?.title}
</h1>
<div className="flex flex-row justify-between items-center space-x-2 text-sm text-black mb-4">
<div className="flex flex-row gap-3">
<div className="text-[#31942E]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g
fill="none"
// fill-rule="evenodd"
>
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.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.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2M8.5 9.5a3.5 3.5 0 1 1 7 0a3.5 3.5 0 0 1-7 0m9.758 7.484A7.99 7.99 0 0 1 12 20a7.99 7.99 0 0 1-6.258-3.016C7.363 15.821 9.575 15 12 15s4.637.821 6.258 1.984"
/>
</g>
</svg>
</div>
<span className="text-[#31942E] font-medium">
{articleDetail?.createdByName}
</span>
<span>-</span>
<span>
<span>
{new Date(articleDetail?.createdAt).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
</span>
</span>
<span className="text-gray-500">in</span>
<span>{articleDetail?.categories?.[0]?.title}</span>
</div>
<div className="flex items-center">
<CommentIcon />0
</div>
</div>
<div className="w-full h-auto mb-6">
{articleDetail?.files?.[0]?.file_url ? (
<Image
src={articleDetail.files[0].file_url}
alt="Berita"
width={800}
height={400}
className="rounded-lg w-full object-cover"
/>
) : (
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p>
</div>
)}
<div className=" flex flex-row w-fit rounded overflow-hidden mr-5 gap-3">
<div className="flex flex-col items-center gap-2">
<p className="text-red-500 font-semibold">0</p>
<p className="text-red-500 font-semibold">SHARES</p>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-black font-semibold">3</p>
<p className="text-black font-semibold">VIEWS</p>
</div>
<Link
href="#"
aria-label="Facebook"
className="bg-[#3b5998] p-4 flex justify-center items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="white"
viewBox="0 0 24 24"
>
<path 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"
className="bg-[#55acee] p-4 flex justify-center items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="white"
viewBox="0 0 24 24"
>
<path 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="WhatsApp"
className="bg-green-700 p-4 flex justify-center items-center text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<g fill="none">
<g
// clip-path="url(#SVGXv8lpc2Y)"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M17.415 14.382c-.298-.149-1.759-.867-2.031-.967s-.47-.148-.669.15c-.198.297-.767.966-.94 1.164c-.174.199-.347.223-.644.075c-.297-.15-1.255-.463-2.39-1.475c-.883-.788-1.48-1.761-1.653-2.059c-.173-.297-.019-.458.13-.606c.134-.133.297-.347.446-.52s.198-.298.297-.497c.1-.198.05-.371-.025-.52c-.074-.149-.668-1.612-.916-2.207c-.241-.579-.486-.5-.668-.51c-.174-.008-.372-.01-.57-.01s-.52.074-.792.372c-.273.297-1.04 1.016-1.04 2.479c0 1.462 1.064 2.875 1.213 3.074s2.095 3.2 5.076 4.487c.71.306 1.263.489 1.694.625c.712.227 1.36.195 1.872.118c.57-.085 1.758-.719 2.006-1.413s.247-1.289.173-1.413s-.272-.198-.57-.347m-5.422 7.403h-.004a9.87 9.87 0 0 1-5.032-1.378l-.36-.214l-3.742.982l.999-3.648l-.235-.374a9.86 9.86 0 0 1-1.511-5.26c.002-5.45 4.436-9.884 9.889-9.884a9.8 9.8 0 0 1 6.988 2.899a9.82 9.82 0 0 1 2.892 6.992c-.002 5.45-4.436 9.885-9.884 9.885m8.412-18.297A11.82 11.82 0 0 0 11.992 0C5.438 0 .102 5.335.1 11.892a11.86 11.86 0 0 0 1.587 5.945L0 24l6.304-1.654a11.9 11.9 0 0 0 5.684 1.448h.005c6.554 0 11.89-5.335 11.892-11.893a11.82 11.82 0 0 0-3.48-8.413"
// clip-rule="evenodd"
/>
</g>
<defs>
<clipPath id="SVGXv8lpc2Y">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</g>
</svg>
</Link>
<Link
href="#"
aria-label="Telegram"
className="bg-blue-400 p-4 flex justify-center items-center text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 256 256"
>
<defs>
<linearGradient
id="SVGuySfwdaH"
x1="50%"
x2="50%"
y1="0%"
y2="100%"
>
<stop
offset="0%"
// stop-color="#2aabee"
/>
<stop
offset="100%"
// stop-color="#229ed9"
/>
</linearGradient>
</defs>
<path
fill="url(#SVGuySfwdaH)"
d="M128 0C94.06 0 61.48 13.494 37.5 37.49A128.04 128.04 0 0 0 0 128c0 33.934 13.5 66.514 37.5 90.51C61.48 242.506 94.06 256 128 256s66.52-13.494 90.5-37.49c24-23.996 37.5-56.576 37.5-90.51s-13.5-66.514-37.5-90.51C194.52 13.494 161.94 0 128 0"
/>
<path
fill="#fff"
d="M57.94 126.648q55.98-24.384 74.64-32.152c35.56-14.786 42.94-17.354 47.76-17.441c1.06-.017 3.42.245 4.96 1.49c1.28 1.05 1.64 2.47 1.82 3.467c.16.996.38 3.266.2 5.038c-1.92 20.24-10.26 69.356-14.5 92.026c-1.78 9.592-5.32 12.808-8.74 13.122c-7.44.684-13.08-4.912-20.28-9.63c-11.26-7.386-17.62-11.982-28.56-19.188c-12.64-8.328-4.44-12.906 2.76-20.386c1.88-1.958 34.64-31.748 35.26-34.45c.08-.338.16-1.598-.6-2.262c-.74-.666-1.84-.438-2.64-.258c-1.14.256-19.12 12.152-54 35.686c-5.1 3.508-9.72 5.218-13.88 5.128c-4.56-.098-13.36-2.584-19.9-4.708c-8-2.606-14.38-3.984-13.82-8.41c.28-2.304 3.46-4.662 9.52-7.072"
/>
</svg>
</Link>
</div>
<p className="text-sm text-gray-500 mt-2 text-start">
{articleDetail?.slug}
</p>
</div>
<div className="flex relative">
<div className="flex-1 overflow-y-auto">
<p className="text-gray-700 leading-relaxed text-justify">
{/* <span className="text-black font-bold text-md">
Mikulnews.com -
</span> */}
{articleDetail?.description}
</p>
<div className="flex flex-row gap-2 items-center">
<span className="font-semibold text-sm text-gray-700">
Tags:
</span>
<div className="flex flex-wrap gap-2 mt-1">
{articleDetail?.tags ? (
<span className="bg-gray-100 text-gray-700 text-sm px-2 py-1 rounded">
{articleDetail.tags}
</span>
) : (
<span className="text-sm text-gray-500">Tidak ada tag</span>
)}
</div>
</div>
</div>
</div>
<div className="relative mb-2 h-[120px] overflow-hidden flex items-center border my-8">
<Image
src={"/image-kolom.png"}
alt="Berita Utama"
fill
className="object-contain"
/>
</div>
<div className="mt-10">
<h2 className="text-2xl font-bold mb-2">Tinggalkan Balasan</h2>
<p className="text-gray-600 mb-4 text-sm">
Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib
ditandai <span className="text-red-500">*</span>
</p>
<form className="space-y-6 mt-6">
<div>
<label
htmlFor="komentar"
className="block text-sm font-medium mb-1"
>
Komentar <span className="text-red-500">*</span>
</label>
<textarea
id="komentar"
className="w-full border border-gray-300 rounded-md p-3 h-40 focus:outline-none focus:ring-2 focus:ring-red-600"
required
/>
</div>
<div>
<label
htmlFor="nama"
className="block text-sm font-medium mb-1"
>
Nama <span className="text-red-500">*</span>
</label>
<input
type="text"
id="nama"
className="w-full border border-gray-300 rounded-md p-2"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium mb-1"
>
Email <span className="text-red-500">*</span>
</label>
<input
type="email"
id="email"
className="w-full border border-gray-300 rounded-md p-2"
required
/>
</div>
<div>
<label
htmlFor="website"
className="block text-sm font-medium mb-1"
>
Situs Web
</label>
<input
type="url"
id="website"
className="w-full border border-gray-300 rounded-md p-2"
/>
</div>
</div>
<div className="flex items-start space-x-2 mt-2">
<input type="checkbox" id="saveInfo" className="mt-1" />
<label htmlFor="saveInfo" className="text-sm text-gray-700">
Simpan nama, email, dan situs web saya pada peramban ini untuk
komentar saya berikutnya.
</label>
</div>
<p className="text-red-600 text-sm">
The reCAPTCHA verification period has expired. Please reload the
page.
</p>
<button
type="submit"
className="bg-red-500 hover:bg-red-700 text-white font-semibold px-6 py-2 rounded-md transition mt-2"
>
KIRIM KOMENTAR
</button>
</form>
</div>
</div>
<div className="md:col-span-1 space-y-6">
<div className="sticky top-0 space-y-6">
<div className="space-y-6">
{articles?.map((article) => (
<div key={article.id}>
<div>
<Link
className="flex space-x-3 mb-2"
href={`/detail/${article.id}`}
>
<Image
src={article.thumbnailUrl || "/default-thumb.png"}
alt={article.title}
width={120}
height={80}
className="rounded object-cover w-[120px] h-[80px]"
/>
<div className="flex-1">
<p className="text-sm font-bold leading-snug hover:text-red-700">
{article.title}
</p>
<div className="flex items-center text-xs text-gray-500 mt-1 space-x-2">
<span>
📅{" "}
{new Date(article.createdAt).toLocaleDateString(
"id-ID",
{
day: "2-digit",
month: "long",
year: "numeric",
}
)}
</span>
<span>💬 0</span>
</div>
</div>
</Link>
</div>
<p className="text-sm text-gray-700 line-clamp-2">
{article.description.slice(0, 120)}...
</p>
</div>
))}
</div>
</div>
</div>
</div>
</>
);
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

2734
components/icons.tsx Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -0,0 +1,173 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { CalendarDays } from "lucide-react";
const categories = [
"Destination",
"Food & Drink",
"News",
"Photo",
"Tak Berkategori",
"Travel Ideas",
"Video",
];
const tags = [
"AI",
"arus balik",
"arus mudik",
"Backpacker",
"Budi Zulkifli",
"drama Korea",
"Food",
"Indonesia",
"Jin BTS",
"jurnalis",
"Kakorlantas",
"konser",
"Korea Utara",
"Lebaran 2025",
"Media Hub",
"mudik",
"musik AI",
"Piala Usia U-17 2025",
"Polri",
"Resident Playbook",
"The Astronaut",
"Tips",
"Tol Cipali",
"Tol Japek",
"Trip Plan",
];
const recentNews = [
{
id: 1,
title:
"Jin BTS Jadi Special Guest Konser Coldplay di Korea, Bawakan Lagu My Universe dan The Astronaut",
date: "21 APRIL 2025",
image: "/coldplay.png",
},
{
id: 2,
title:
"Kehadiran Media Hub Polri Jadi Jawaban atas Kebutuhan Jurnalis Masa Kini",
date: "16 APRIL 2025",
image: "/mediahub-1.jpg",
},
];
export default function Footer() {
return (
<footer className="bg-[#293132] text-gray-300 text-sm">
<div className="max-w-7xl mx-auto px-6 py-12 grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Logo & About */}
<div>
<h2 className="text-2xl font-bold">
<span className="text-white">travel</span>
<span className="text-teal-400">news</span>
</h2>
<p className="mt-3 text-gray-400 leading-relaxed">
We bring you the best Premium WordPress Themes that perfect for
news, magazine, personal blog, etc.
</p>
<Link
href="#"
className="mt-3 inline-block text-teal-400 hover:underline"
>
Read more
</Link>
</div>
{/* Categories */}
<div>
<h3 className="text-white font-semibold mb-3">Categories</h3>
<ul className="space-y-2">
{categories.map((cat, i) => (
<li key={i}>
<Link href="#" className="hover:text-teal-400">
{cat}
</Link>
</li>
))}
</ul>
</div>
{/* Tags */}
<div>
<h3 className="text-white font-semibold mb-3">Tags</h3>
<div className="flex flex-wrap gap-2">
{tags.map((tag, i) => (
<span
key={i}
className="bg-gray-700 hover:bg-teal-500 hover:text-white text-gray-300 text-xs px-2 py-1 rounded cursor-pointer"
>
{tag}
</span>
))}
</div>
</div>
{/* Recent News */}
<div>
<h3 className="text-white font-semibold mb-3">Recent News</h3>
<ul className="space-y-4">
{recentNews.map((news) => (
<li key={news.id} className="flex gap-3">
<Image
src={news.image}
alt={news.title}
width={80}
height={60}
className="object-cover rounded"
/>
<div>
<Link
href="#"
className="text-gray-200 font-medium hover:text-teal-400 text-sm leading-snug"
>
{news.title}
</Link>
<div className="flex items-center text-xs text-gray-400 gap-1 mt-1">
<CalendarDays size={12} />
{news.date}
</div>
</div>
</li>
))}
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t bg-[#474044] border-gray-700 py-4 px-6 flex flex-col md:flex-row items-center justify-between text-xs text-gray-400">
<p>
© 2025 <span className="text-teal-400">JNews</span> - Premium
WordPress news & magazine theme by{" "}
<Link href="#" className="hover:underline">
Jegtheme
</Link>
.
</p>
<div className="flex gap-4 mt-2 md:mt-0">
<Link href="#">
<i className="fab fa-facebook"></i>
</Link>
<Link href="#">
<i className="fab fa-twitter"></i>
</Link>
<Link href="#">
<i className="fab fa-google-plus-g"></i>
</Link>
<Link href="#">
<i className="fab fa-pinterest"></i>
</Link>
<Link href="#">
<i className="fab fa-rss"></i>
</Link>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,109 @@
"use client";
import Image from "next/image";
import { CalendarDays } from "lucide-react";
import { useEffect, useState } from "react";
import { getListArticle } from "@/service/article";
import Link from "next/link";
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 Beranda() {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("4");
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() {
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
return (
<section className="px-6 py-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{articles.map((item) => (
<div key={item.id}>
<Link
className="relative overflow-hidden group cursor-pointer"
href={`/detail/${item?.id}`}
>
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
width={500}
height={430}
className="object-cover w-full h-[430px] group-hover:scale-105 transition-transform duration-500"
/>
{/* Overlay */}
<div className="absolute inset-0 bg-black/50 flex flex-col justify-end p-4">
{/* Category */}
<span className="absolute top-3 left-3 bg-red-500 text-white text-xs px-2 py-1 rounded">
{item.categories?.[0]?.title || "TAK BERKATEGORI"}
</span>
{/* Title */}
<h2 className="text-white font-bold text-lg leading-snug mb-2">
{item.title}
</h2>
{/* Date */}
<div className="flex items-center text-white text-xs gap-2">
<CalendarDays size={14} />
<span>
{new Date(item.createdAt).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</span>
</div>
</div>
</Link>
</div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,119 @@
"use client";
import { getListArticle } from "@/service/article";
import { Timer } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
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;
}[];
};
const MoreNews = () => {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("4");
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() {
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
return (
<section className="max-w-7xl mx-auto py-16 bg-white">
<h2 className="text-xl font-semibold mb-6 border-b pb-2 relative">
More News{" "}
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map((item) => (
<div key={item?.id} className="space-y-2">
<Link className="space-y-2" href={`/detail/${item?.id}`}>
<div className="relative">
<Image
src={item.thumbnailUrl}
alt={item.title}
width={400}
height={250}
className="w-full h-48 object-cover rounded-md"
/>
<span className="absolute bottom-2 left-2 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded">
{item.categories?.[0]?.title}
</span>
</div>
<h3 className="text-sm font-semibold leading-snug hover:text-teal-600 cursor-pointer">
{item.title}
</h3>
<p className="text-gray-500 text-xs flex items-center gap-1">
<Timer className="text-teal-600" size={15} />
{item.createdAt}
</p>
</Link>
</div>
))}
</div>
<div className="mt-10 flex items-center justify-center gap-4">
{/* Garis Kiri */}
<div className="h-px bg-gray-300 flex-1" />
{/* Tombol */}
<button className="px-6 py-2 border border-gray-300 rounded text-sm hover:bg-gray-100 transition">
LOAD MORE
</button>
{/* Garis Kanan */}
<div className="h-px bg-gray-300 flex-1" />
</div>
<div className="max-w-7xl relative my-5 h-[125px] overflow-hidden flex items-center mx-auto border">
<Image
src="/image-kolom.png"
alt="Berita Utama"
fill
className="object-cover"
/>
</div>
</section>
);
};
export default MoreNews;

View File

@ -0,0 +1,74 @@
"use client";
import { useState } from "react";
import { Search, ShoppingCart, Lock } from "lucide-react";
import Link from "next/link";
export default function Navbar() {
const [date] = useState(() => {
const now = new Date();
return new Intl.DateTimeFormat("id-ID", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(now);
});
return (
<header className="w-full border-b border-gray-200">
{/* Topbar */}
<div className="flex justify-between items-center bg-teal-400 text-white text-sm px-6 py-2">
<span>{date}</span>
<div className="flex space-x-4 items-center">
<Link href="/shop">Shop</Link>
<Link href="/account">My Account</Link>
<Link href="/contact">Contact Us</Link>
<Link href="/auth" className="flex items-center gap-1">
<Lock size={14} /> Login
</Link>
</div>
</div>
{/* Main Navbar */}
<div className="flex items-center justify-between px-6 py-4 bg-white">
{/* Logo */}
<Link href="/" className="text-2xl font-bold">
<span className="text-gray-900">travel</span>
<span className="text-teal-400">news</span>
</Link>
{/* Menu */}
<nav className="hidden md:flex space-x-6 font-semibold text-sm">
<Link href="/" className="hover:text-teal-400">
HOME
</Link>
<Link href="/travel-news" className="hover:text-teal-400">
TRAVEL NEWS
</Link>
<Link href="/destination" className="hover:text-teal-400">
DESTINATION
</Link>
<Link href="/travel-ideas" className="hover:text-teal-400">
TRAVEL IDEAS
</Link>
<Link href="/food-drink" className="hover:text-teal-400">
FOOD & DRINK
</Link>
<Link href="/video" className="hover:text-teal-400">
VIDEO
</Link>
</nav>
{/* Icons */}
<div className="flex items-center space-x-4">
<button aria-label="Search">
<Search size={20} className="text-gray-700" />
</button>
<button aria-label="Cart">
<ShoppingCart size={20} className="text-gray-700" />
</button>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,707 @@
"use client";
import Image from "next/image";
import { CalendarDays, MessageCircle, Timer } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getListArticle } from "@/service/article";
const featured = [
{
id: 1,
title: "Tak Perpanjang Kontrak, Megawati Hengkang dari Red Sparks",
date: "11 APRIL 2025",
image: "/megawati.png",
},
{
id: 2,
title: "Polri Dinilai Sukses Kelola Arus Mudik dan Balik Lebaran 2025",
date: "11 APRIL 2025",
image: "/mudik.png",
},
];
const smallNews = [
{
id: 3,
title:
"Arus Mudik dan Balik Libur Lebaran 2025 Lancar, Kerja Keras Polri Panen Apresiasi",
date: "8 APRIL 2025",
image: "/mudik.png",
},
{
id: 4,
title:
"1 Juta Penonton dalam Seminggu, Jumbo Pecahkan Rekor Animasi Indonesia",
date: "8 APRIL 2025",
image: "/jumbo.png",
},
{
id: 5,
title:
"Jalur Fungsional Tol Jakarta-Cikampek II Selatan Siap Layani Pemudik",
date: "8 APRIL 2025",
image: "/tol.jpg",
},
{
id: 6,
title:
"Arus Balik Diprediksi 5-6 April, Polri Imbau Pemudik Siapkan Perjalanan",
date: "8 APRIL 2025",
image: "/mudik.png",
},
];
const featured1 = [
{
id: 1,
title: "Explore the Beauty of Bali: Beaches, Culture, and More",
category: "TRAVEL IDEAS",
date: "August 30, 2025",
image: "/summer.jpg",
},
];
const smallNews1 = [
{
id: 2,
title: "Top 5 Destinations for Your Summer Getaway",
category: "DESTINATION",
date: "August 28, 2025",
image: "/blogger.jpg",
},
{
id: 3,
title: "How to Travel Safely During the Holiday Season",
category: "NEWS",
date: "August 25, 2025",
image: "/trip.jpg",
},
];
const posts = [
{
id: 1,
title: "Your Guide to Canggus Hottest Street: The Essential Batu Bolong",
author: "Admin",
date: "27 November 2024",
comments: 0,
excerpt:
"Dropcap the popularization of the “ideal measure” has led to advice such as “Increase font size for large screens and...",
image: "/guide.jpg",
},
{
id: 2,
title: "6 Perfect places to watch the sunrise in Bali while you honeymoon",
author: "Admin",
date: "26 November 2024",
comments: 0,
excerpt:
"Intro text we refine our methods of responsive web design, weve increasingly focused on measure and its relationship to how...",
image: "/car.jpg",
},
];
const posts1 = [
{
id: 1,
title: "Great Surf Spots in Bali: From Beginner to Pro Surfers",
author: "Admin",
date: "27 November 2024",
comments: 0,
excerpt:
"Intro text we refine our methods of responsive web design, weve increasingly focused on measure and its relationship to how...",
image: "/tea.jpg",
},
{
id: 2,
title: "Important things you should know for Mount Agung hiking",
author: "Admin",
date: "26 November 2024",
comments: 0,
excerpt:
"Dropcap the popularization of the “ideal measure” has led to advice such as “Increase font size for large screens and...",
image: "/play.png",
},
];
const popularPosts = [
{
id: 1,
title:
"Kisah Cinta IU-Park Bo Gum di 1950-an Hadir di When Life Gives You Tangerines, Tayang 7 Maret 2025",
image: "/park-go.jpg",
shares: 0,
},
{
id: 2,
title:
"Temani Perjalanan Mudik Anda dengan Musik AI dari Qudo Buana Nawakara",
shares: 0,
},
{
id: 3,
title: "Bali Nightlife Guide : The most popular clubs in Kuta",
shares: 0,
},
{
id: 4,
title:
"Dirumorkan Hubungan dengan Kim Ji Won, Kim Soo Hyun Langsung Klarifikasi Begini",
shares: 0,
},
{
id: 5,
title:
"Carmen Idol K-Pop Asal Bali yang Resmi Debut dengan Girl Grup Baru SM Entertainment",
shares: 0,
},
];
const categories = [
{ id: 1, name: "DESTINATION", count: 22 },
{ id: 2, name: "FOOD & DRINK", count: 8 },
{ id: 3, name: "NEWS", count: 21 },
{ id: 4, name: "PHOTO", count: 17 },
{ id: 5, name: "TAK BERKATEGORI", count: 34 },
{ id: 6, name: "TRAVEL IDEAS", count: 27 },
{ id: 7, name: "VIDEO", count: 7 },
];
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 News() {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("4"); // 👈 ambil 4 artikel
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() {
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
return (
<section className="max-w-7xl mx-auto py-12 grid grid-cols-1 lg:grid-cols-3 gap-10">
{/* Left Column */}
<div className="lg:col-span-2">
{/* Section Title */}
<div className="flex items-center justify-between border-b border-gray-200 pb-2 mb-6">
<h2 className="text-xl font-semibold relative">
Travel Ideas
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h2>
<div className="flex gap-4 text-sm text-gray-500 font-medium">
<Link href="#" className="hover:text-teal-500">
ALL
</Link>
<Link href="#" className="hover:text-teal-500">
NEWS
</Link>
<Link href="#" className="hover:text-teal-500">
TRAVEL IDEAS
</Link>
<Link href="#" className="hover:text-teal-500">
DESTINATION
</Link>
<Link href="#" className="hover:text-teal-500">
PHOTO
</Link>
</div>
</div>
{/* Featured News */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{articles.slice(0, 2).map((item) => (
<div key={item.id}>
<Link
className="relative rounded overflow-hidden group cursor-pointer"
href={`/detail/${item?.id}`}
>
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
width={500}
height={300}
className="w-full h-[250px] object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-black/50 flex flex-col justify-end p-4">
<h3 className="text-white font-bold text-lg leading-snug">
{item.title}
</h3>
<div className="flex items-center text-white text-xs gap-2 mt-2">
<CalendarDays size={14} />
<span>
{new Date(item.createdAt).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</span>
</div>
</div>
</Link>
</div>
))}
</div>
{/* Small News */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((item) => (
<div key={item.id}>
<Link className="flex gap-4" href={`/detail/${item?.id}`}>
<Image
src={item.thumbnailUrl}
alt={item.title}
width={120}
height={80}
className="rounded object-cover flex-shrink-0"
/>
<div>
<h4 className="text-sm font-semibold text-gray-800 hover:text-teal-500 leading-snug">
{item.title}
</h4>
<div className="flex items-center text-xs text-gray-500 gap-2 mt-1">
<CalendarDays size={12} />
<span>{item.createdAt}</span>
</div>
</div>
</Link>
</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>
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-200 pb-2 mb-6">
<h2 className="text-xl font-semibold relative">
Travel Destination
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h2>
<div className="flex gap-4 text-sm text-gray-500 font-medium">
<Link href="#" className="hover:text-teal-500">
ALL
</Link>
<Link href="#" className="hover:text-teal-500">
NEWS
</Link>
<Link href="#" className="hover:text-teal-500">
TRAVEL IDEAS
</Link>
<Link href="#" className="hover:text-teal-500">
DESTINATION
</Link>
<Link href="#" className="hover:text-teal-500">
VIDEO
</Link>
</div>
</div>
{/* Featured News */}
<div>
{/* Featured News Besar */}
<div className="mb-6">
<div className="relative rounded overflow-hidden group cursor-pointer">
{articles.length > 0 && (
<>
<Link
className="flex gap-4"
href={`/detail/${articles[0]?.id}`}
>
<Image
src={articles[0]?.thumbnailUrl || "/placeholder.jpg"}
alt={articles[0]?.title || "No title"}
width={800}
height={400}
className="w-full h-[400px] object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-black/40 flex flex-col justify-end p-6">
<span className="bg-red-500 text-white text-xs px-2 py-1 rounded mb-2 w-fit uppercase">
{articles[0]?.categories?.[0]?.title ||
"TAK BERKATEGORI"}
</span>
<h3 className="text-white font-bold text-2xl leading-snug">
{articles[0]?.title}
</h3>
<div className="flex items-center text-white text-xs gap-2 mt-2">
<span className="font-semibold">
BY {articles[0]?.createdByName || "ADMIN"}
</span>
<CalendarDays size={14} />
<span>
{new Date(
articles[0]?.createdAt || ""
).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</span>
</div>
</div>
</Link>
</>
)}
</div>
</div>
{/* Small News */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.slice(1, 3).map((item) => (
<div key={item.id}>
<Link
className="relative rounded overflow-hidden group cursor-pointer"
href={`/detail/${item?.id}`}
>
<Image
src={item.thumbnailUrl}
alt={item.title}
width={400}
height={200}
className="w-full h-[250px] object-cover group-hover:scale-105 transition-transform duration-500"
/>
{/* Badge kategori di atas gambar */}
<div className="absolute top-3 left-3">
<span className="bg-red-500 text-white text-xs px-2 py-1 rounded w-fit uppercase">
{item.categories?.[0]?.title}
</span>
</div>
{/* Content di bawah gambar */}
<div className="mt-3">
<h4 className="text-black font-semibold text-base leading-snug">
{item.title}
</h4>
<div className="flex items-center text-xs text-gray-500 gap-2 mt-1">
<span className="font-semibold text-gray-700">
BY <span className="text-[#49C5C4]">ADMIN</span>
</span>
<Timer className="text-[#49C5C4]" size={12} />
<span>{item.createdAt}</span>
</div>
</div>
</Link>
</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 items-center justify-between border-b border-gray-200 pb-2 mb-6">
<h2 className="text-xl font-semibold relative">
Travel Destination
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h2>
<div className="flex gap-4 text-sm text-gray-500 font-medium">
<Link href="#" className="hover:text-teal-500">
ALL
</Link>
<Link href="#" className="hover:text-teal-500">
NEWS
</Link>
<Link href="#" className="hover:text-teal-500">
TRAVEL IDEAS
</Link>
<Link href="#" className="hover:text-teal-500">
DESTINATION
</Link>
<Link href="#" className="hover:text-teal-500">
VIDEO
</Link>
</div>
</div>
{/* Featured News */}
<div>
{/* Featured News Besar */}
<div className="space-y-8">
{articles.slice(2, 4).map((post) => (
<div
key={post.id}
className="flex flex-col md:flex-row border-b border-gray-200 pb-6"
>
{/* Left Content */}
<div className="flex-1 pr-0 md:pr-6">
<h2 className="text-xl md:text-2xl font-bold mb-2">
{post.title}
</h2>
<div className="flex items-center text-sm text-gray-500 gap-2 mb-3">
<span>
BY{" "}
<span className="text-[#49C5C4] font-semibold uppercase">
{post.createdByName}
</span>
</span>
<span></span>
<span>{post.createdAt}</span>
<span></span>
<MessageCircle size={14} className="text-[#49C5C4]" />
<span>{/* {post.comments} */}0</span>
</div>
<p className="text-gray-600 mb-4">
{post.description.split(" ").slice(0, 50).join(" ")}
{post.description.split(" ").length > 50 && "..."}
</p>
<Link href={`/detail/${post?.id}`}>
<button className="px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-100 transition">
READ MORE
</button>
</Link>
</div>
{/* Right Thumbnail */}
<div className="w-full md:w-[280px] flex-shrink-0 mt-4 md:mt-0">
<Image
src={post.thumbnailUrl}
alt={post.title}
width={400}
height={250}
className="w-full h-[180px] md:h-[150px] object-cover rounded"
/>
</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>
{/* Featured News Besar */}
<div className="space-y-8">
{articles.slice(1, 3).map((post) => (
<div
key={post.id}
className="flex flex-col md:flex-row border-b border-gray-200 pb-6"
>
{/* Left Content */}
<div className="flex-1 pr-0 md:pr-6">
<h2 className="text-xl md:text-2xl font-bold mb-2">
{post.title}
</h2>
<div className="flex items-center text-sm text-gray-500 gap-2 mb-3">
<span>
BY{" "}
<span className="text-[#49C5C4] font-semibold uppercase">
{post.createdByName}
</span>
</span>
<span></span>
<span>{post.createdAt}</span>
<span></span>
<MessageCircle size={14} className="text-[#49C5C4]" />
<span>{/* {post.comments} */}0</span>
</div>
<p className="text-gray-600 mb-4">
{post.description.split(" ").slice(0, 50).join(" ")}
{post.description.split(" ").length > 50 && "..."}
</p>
<Link href={`/detail/${post?.id}`}>
<button className="px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-100 transition">
READ MORE
</button>
</Link>
</div>
{/* Right Thumbnail */}
<div className="w-full md:w-[280px] flex-shrink-0 mt-4 md:mt-0">
<Image
src={post.thumbnailUrl}
alt={post.title}
width={400}
height={250}
className="w-full h-[180px] md:h-[150px] object-cover rounded"
/>
</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>
{/* Sidebar */}
<aside className="space-y-6">
{/* Connect with us */}
<div>
<h3 className="font-semibold border-b border-gray-200 pb-2 mb-4">
Connect with us
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center border px-4 py-3 rounded">
<span className="font-medium"></span>
<span>140 Followers</span>
</div>
<div className="flex justify-between items-center border px-4 py-3 rounded">
<span className="text-red-500 font-medium">YouTube</span>
<span>204k Subscribers</span>
</div>
</div>
</div>
{/* Advertisement */}
<div>
<Image
src="/advertisiment.png"
alt="Advertisement"
width={345}
height={345}
className="w-full rounded"
/>
</div>
{/* Popular (Placeholder) */}
<div>
<h3 className="font-semibold border-b border-gray-200 pb-2 mb-4 relative">
Popular
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h3>
{popularPosts.map((post, index) =>
index === 0 ? (
// Post pertama tampil besar
<div key={post.id} className="mb-6 relative">
<Image
src={post.image || "/images/placeholder.jpg"}
alt={post.title}
width={400}
height={250}
className="w-full h-[246px] object-cover rounded"
/>
<h2 className="mt-3 text-base font-semibold">{post.title}</h2>
<div className="flex items-center text-xs text-gray-500 mt-1">
<span>{post.shares} SHARES</span>
</div>
<span className="absolute bottom-2 right-2 text-3xl font-bold text-gray-200">
{String(index + 1).padStart(2, "0")}
</span>
</div>
) : (
// Post berikutnya tampil list
<div
key={post.id}
className="flex items-start gap-3 border-t border-gray-200 py-3"
>
<span className="text-lg font-bold text-gray-400">
{String(index + 1).padStart(2, "0")}
</span>
<div>
<h3 className="text-sm font-medium leading-snug">
{post.title}
</h3>
<span className="text-xs text-gray-500">
{post.shares} SHARES
</span>
</div>
</div>
)
)}
</div>
<h3 className="font-semibold border-b border-gray-200 pb-2 mb-4 mt-10 relative">
Categories
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h3>
<ul className="space-y-2">
{categories.map((cat) => (
<li
key={cat.id}
className="flex justify-between items-center border-b border-gray-100 pb-2"
>
<span className="text-sm hover:text-teal-600 cursor-pointer font-medium">
{cat.name}
</span>
<span className="text-sm text-gray-500">({cat.count})</span>
</li>
))}
</ul>
<h3 className="font-semibold border-b border-gray-200 pb-2 mb-4 mt-10 relative">
Instagram
<span className="absolute bottom-[-2px] left-0 w-16 h-[2px] bg-teal-400"></span>
</h3>
<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 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>
</section>
);
}

View File

@ -0,0 +1,186 @@
// components/landing-page/opini.tsx
"use client";
import Image from "next/image";
type NewsItem = {
id: number;
title: string;
date: string;
image?: string;
};
type NewsColumnProps = {
title: string;
color: string;
items: NewsItem[];
};
const data = {
pelayanan: {
title: "Pelayanan",
color: "bg-red-600",
items: [
{
id: 1,
title:
"Polres Rokan Hulu dan Dukungan Program Makan Bergizi Gratis SDN 016 Sei Bungo",
date: "28 JANUARI 2025",
image: "/rokanhulu.png",
},
{
id: 2,
title: "Polri Buka Rekrutmen Khusus Lulusan Pertanian dan Peternakan",
date: "4 NOVEMBER 2024",
},
{
id: 3,
title:
"Polri Siap Dukung Presiden Prabowo Subianto Menuju Ketahanan Pangan Nasional Indonesia",
date: "24 OKTOBER 2024",
},
{
id: 4,
title:
"Divisi Humas Polri dan TVRI Gelar Pelatihan Presenter untuk Optimalisasi Komunikasi Publik",
date: "15 JULI 2024",
},
],
},
opini: {
title: "Opini",
color: "bg-red-600",
items: [
{
id: 1,
title:
"KH Abu Bakar Abdul Jalil Apresiasi Rekrutmen Jalur Khusus di Polri bagi Santri dan Hafiz Al-Quran",
date: "25 FEBRUARI 2025",
image: "/abubakar.png",
},
{
id: 2,
title:
"Polisi Istimewa Pejuang Kemerdekaan ke Peringatan Hari Juang Polri",
date: "8 NOVEMBER 2024",
},
{
id: 3,
title:
"Menkodmigi Meutya Hafid Apresiasi Polri dalam Pemberantasan Judi Online di Indonesia",
date: "1 NOVEMBER 2024",
},
{
id: 4,
title:
"Komjen Purn Boy Rafli Amar Bicara Inovasi Humas Polri dalam Meningkatkan Kepercayaan Publik dan Pelayanan Prima Kepolisian",
date: "31 OKTOBER 2024",
},
],
},
inspirasi: {
title: "Inspirasi",
color: "bg-red-600",
items: [
{
id: 1,
title:
"Apa yang dimaksud Bhayangkari? Apa Sejarah, Fungsi dan Perannya",
date: "20 FEBRUARI 2023",
image: "/bhayangkari.jpg",
},
{
id: 2,
title: "Lirik Lagu Mars Bhayangkari",
date: "6 JULI 2023",
},
{
id: 3,
title:
"Pendidikan Minimal Istri Polisi di Indonesia: Syarat dan Peranannya",
date: "4 JULI 2023",
},
{
id: 4,
title: "Polri Gowes Presisi Khatulistiwa Cetak Rekor MURI",
date: "27 JUNI 2022",
},
],
},
};
function NewsColumn({ title, color, items }: NewsColumnProps) {
return (
<div className="bg-transparent p-4">
{/* Header */}
<div className="flex items-center mb-4">
<div className={`${color} text-white px-3 py-1 font-semibold rounded`}>
{title}
</div>
<div className="flex-1 border-t border-gray-300 ml-2" />
</div>
<div className="space-y-4">
{items.map((item, index: number) => (
<div
key={item.id}
className={`flex flex-col ${
index === 0 ? "md:flex-row" : ""
} border-b border-gray-200 pb-3`}
>
{index === 0 && item?.image && (
<div className="w-full md:w-40 h-24 relative mr-3">
<Image
src={item?.image}
alt={item?.title}
fill
className="object-cover rounded"
/>
</div>
)}
<div className="flex-1">
<h3 className="text-sm font-medium hover:text-blue-600 cursor-pointer">
{item.title}
</h3>
<p className="text-xs text-gray-500 mt-1 flex items-center">
<span className="mr-1">🕒</span> {item.date}
</p>
</div>
</div>
))}
</div>
<div className="flex justify-start items-center mt-4">
<button className="px-3 py-1 text-xs border rounded bg-white text-gray-600 hover:bg-gray-200">
&lt; PREV
</button>
<button className="px-3 py-1 text-xs border rounded bg-white text-gray-600 hover:bg-gray-200">
NEXT &gt;
</button>
</div>
</div>
);
}
export default function Opini() {
return (
<div className="max-w-7xl mx-auto">
<div className="bg-gray-100 p-6 ">
<div className="grid grid-cols-1 md:grid-cols-3 ">
<NewsColumn {...data.pelayanan} />
<NewsColumn {...data.opini} />
<NewsColumn {...data.inspirasi} />
</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>
);
}

View File

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

View File

@ -0,0 +1,626 @@
"use client";
import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import Link from "next/link";
import DashboardContainer from "../main/dashboard/dashboard-container";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { useTheme } from "../layout/theme-context";
import Option from "./option";
interface RetractingSidebarProps {
sidebarData: boolean;
updateSidebarData: (newData: boolean) => void;
}
const sidebarSections = [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
title: "Content Management",
items: [
{
title: "Articles",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/article",
},
{
title: "Categories",
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
link: "/admin/master-category",
},
// {
// title: "Majalah",
// icon: () => <Icon icon="emojione-monotone:newspaper" className="text-lg" />,
// link: "/admin/magazine",
// },
{
title: "Advertisements",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/advertise",
},
// {
// title: "Komentar",
// icon: () => <Icon icon="material-symbols:comment-outline-rounded" className="text-lg" />,
// link: "/admin/komentar",
// },
],
},
{
title: "System",
items: [
{
title: "Static Pages",
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
link: "/admin/static-page",
},
{
title: "User Management",
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
link: "/admin/master-user",
},
],
},
];
export const RetractingSidebar = ({
sidebarData,
updateSidebarData,
}: RetractingSidebarProps) => {
const pathname = usePathname();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{/* DESKTOP SIDEBAR */}
<AnimatePresence mode="wait">
<motion.nav
key="desktop-sidebar"
layout
className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-r border-slate-200/60 dark:border-slate-700/60 shadow-lg backdrop-blur-sm flex-col justify-between"
style={{
width: sidebarData ? "280px" : "80px",
}}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<SidebarContent
open={sidebarData}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.nav>
</AnimatePresence>
{/* Desktop Toggle Button - appears when sidebar is collapsed */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="desktop-toggle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="hidden md:flex fixed top-4 left-20 z-40 p-3 bg-white rounded-xl shadow-lg border border-slate-200/60 hover:shadow-xl transition-all duration-200 hover:bg-slate-50"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* Mobile Toggle Button */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="mobile-toggle"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
className="md:hidden fixed top-4 left-4 z-50 p-3 bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200/60 dark:border-slate-700/60 hover:shadow-xl transition-all duration-200"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-6 h-6 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* MOBILE SIDEBAR */}
<AnimatePresence>
{sidebarData && (
<motion.div
key="mobile-sidebar"
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "tween", duration: 0.3 }}
className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 p-4 flex flex-col md:hidden shadow-2xl backdrop-blur-sm"
>
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
</button> */}
<SidebarContent
open={true}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
};
const SidebarContent = ({
open,
pathname,
updateSidebarData,
}: {
open: boolean;
pathname: string;
updateSidebarData: (newData: boolean) => void;
}) => {
const { theme, toggleTheme } = useTheme();
const [username, setUsername] = useState("");
useEffect(() => {
// Ambil cookie secara client-side
const cookies = document.cookie.split("; ").reduce((acc: any, cur) => {
const [key, value] = cur.split("=");
acc[key] = value;
return acc;
}, {});
setUsername(cookies.username || "Guest");
}, []);
return (
<div className="flex flex-col h-full">
{/* SCROLLABLE TOP SECTION */}
<div className="flex-1 overflow-y-auto">
{/* HEADER SECTION */}
<div className="flex flex-col space-y-6">
{/* Logo and Toggle */}
<div className="flex items-center justify-between px-4 py-6">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
<img
src="/bhayangkarakita.png"
className="w-10 h-10 rounded-lg shadow-sm"
/>
<div className="absolute -inset-1 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg opacity-20 blur-sm"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col"
>
<span className="text-sm font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
Milenial Bersuara
</span>
<span className="text-xs text-slate-500">Admin Panel</span>
</motion.div>
)}
</Link>
{open && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 group"
onClick={() => updateSidebarData(false)}
>
<Icon
icon="heroicons:chevron-left"
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
/>
</motion.button>
)}
</div>
{/* Navigation Sections */}
<div className="space-y-3 px-3 pb-6">
{sidebarSections.map((section, sectionIndex) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + sectionIndex * 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 + sectionIndex * 0.1 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-2">
{section.items.map((item, itemIndex) => (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
))}
</div>
</motion.div>
))}
</div>
</div>
</div>
{/* FIXED BOTTOM SECTION */}
<div className="flex-shrink-0 space-y-1 border-t border-slate-200/60 dark:border-slate-700/60 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm">
{/* Divider */}
{/* <div className="px-3 pb-2">
<div className="h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent"></div>
</div> */}
{/* Theme Toggle */}
<div className="px-3 pt-1">
<motion.button
onClick={toggleTheme}
className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
open ? "px-3" : "justify-center"
} ${
theme === "dark"
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700/50"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<motion.div
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div
className={`text-lg transition-all duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-500 group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200"
}`}
>
{theme === "dark" ? (
<Icon icon="solar:sun-bold" className="text-lg" />
) : (
<Icon icon="solar:moon-bold" className="text-lg" />
)}
</div>
</motion.div>
{open && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
theme === "dark"
? "text-white"
: "text-slate-700 dark:text-slate-300"
}`}
>
{theme === "dark" ? "Light Mode" : "Dark Mode"}
</motion.span>
)}
</motion.button>
</div>
{/* Settings */}
<div className="px-3">
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>
</div>
{/* User Profile */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="px-3 py-3 border-t border-slate-200/60"
>
<div
className={`${
open
? "flex items-center space-x-3"
: "flex items-center justify-center"
} p-3 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100/50 hover:from-slate-100 hover:to-slate-200/50 transition-all duration-200 cursor-pointer group`}
>
<div className="relative">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
A
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex-1 min-w-0"
>
<p className="text-sm font-medium text-slate-800 truncate">
{username}
</p>
<Link href="/auth">
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
Sign out
</p>
</Link>
</motion.div>
)}
</div>
</motion.div>
{/* Expand Button for Collapsed State */}
{/* {!open && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
className="px-3 pt-2"
>
<button
onClick={() => updateSidebarData(true)}
className="w-full p-3 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg transition-all duration-200 hover:shadow-xl group"
>
<div className="flex items-center justify-center">
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 group-hover:scale-110 transition-transform duration-200"
/>
</div>
</button>
</motion.div>
)} */}
</div>
</div>
);
};
const Sidebar = () => {
const [open, setOpen] = useState(true);
const pathname = usePathname();
const [username, setUsername] = useState("");
useEffect(() => {
// Ambil cookie secara client-side
const cookies = document.cookie.split("; ").reduce((acc: any, cur) => {
const [key, value] = cur.split("=");
acc[key] = value;
return acc;
}, {});
setUsername(cookies.username || "Guest");
}, []);
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>{username}</p>
<p className="underline">Logout</p>
</div>
</div>
</div>
</motion.nav>
);
};
export default Sidebar;
const TitleSection = ({ open }: { open: boolean }) => {
return (
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-slate-100">
<div className="flex items-center">
<motion.div
layout
initial={{ opacity: 0, y: 12, scale: 0.5 }}
animate={
open
? { opacity: 1, y: 0, scale: 1 }
: { opacity: 1, y: 0, scale: 0.5 }
}
transition={{ delay: 0.125 }}
>
<Image
src="/assets/icon/Logo.png"
alt="logo"
width={1920}
height={1080}
className="w-full h-fit"
/>
</motion.div>
</div>
{/* {open && <FiChevronDown className="mr-2" />} */}
</div>
);
};
const ToggleClose = ({
open,
setOpen,
}: {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}) => {
return (
<motion.button layout onClick={() => setOpen((pv) => !pv)}>
<div className="flex justify-center items-center pt-2">
<motion.div layout className="grid size-10 text-lg">
{/* <FiChevronsRight
className={`transition-transform ${open && "rotate-180"}`}
/> */}
</motion.div>
{/* {open && (
<motion.span layout initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.125 }} className="text-xs font-medium">
Hide
</motion.span>
)} */}
</div>
</motion.button>
);
};
const ExampleContent = () => (
<div>
<DashboardContainer />
</div>
);

View File

@ -0,0 +1,144 @@
"use client";
import { getListArticle } from "@/service/article";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
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;
}[];
};
const TravelNews = () => {
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [articles, setArticles] = useState<Article[]>([]);
const [showData, setShowData] = useState("4");
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() {
const req = {
limit: showData,
page,
search,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
isPublish: true,
sortBy: "created_at",
};
try {
const res = await getListArticle(req);
setArticles(res?.data?.data || []);
setTotalPage(res?.data?.meta?.totalPage || 1);
} catch (err) {
console.error("Error fetching articles:", err);
}
}
return (
<section className="bg-white">
<div className="relative py-16 px-4 md:px-20">
<div
className="absolute inset-0 bg-cover bg-center z-0 opacity-90 h-[660px]"
style={{ backgroundImage: "url('/travel.jpg')" }}
/>
<div className="relative z-10 pt-48">
<h2 className="text-4xl font-bold text-white text-start mb-4">
Travel News
</h2>
<p className="text-white text-start mb-12 text-lg">
Find your best tour and start the adventure at the lowest budget and
best experiences
</p>
{/* Top Featured News */}
<div className="grid md:grid-cols-3 gap-6">
{/* CARD 1 */}
{articles.slice(0, 3).map((item) => (
<div key={item.id}>
<Link
className="relative rounded-md overflow-hidden shadow-lg"
href={`/detail/${item?.id}`}
>
<Image
src={item.thumbnailUrl || "/placeholder.jpg"}
alt={item.title}
width={600}
height={400}
className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent p-4 flex flex-col justify-end">
<h3 className="text-white text-lg font-semibold leading-snug">
{item.title}
</h3>
<p className="text-white text-sm mt-2">
{new Date(item.createdAt).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</p>
</div>
</Link>
</div>
))}
</div>
{/* Bottom News List */}
</div>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm max-w-7xl mx-auto mb-10">
{[
"Your Guide to Canggus Hottest Street: The Essential Batu Bolong",
"Expert Tips: How To Become A Professional Travel Blogger",
"Important things you should know for Mount Agung hiking",
"How to build a freelance career while traveling the world",
"Bali Nightlife Guide : The most popular clubs in Kuta",
"This English breakfast pie is maybe the most British thing ever",
"Everything You Need to Know About Solo Travel",
"10 things you didnt know about being solo traveler in Asia",
"Kehadiran Media Hub Polri Jadi Jawaban atas Kebutuhan Jurnalis Masa Kini",
"Indonesia Tersingkir dari Piala Asia U-17 2025 Setelah Kalah Telak 6-0 dari Korea Utara",
"Tayang Besok, Gen 99 Hospital Playlist Juga Jadi Cameo di Resident Playbook",
"Tak Perpanjang Kontrak, Megawati Hengkang dari Red Sparks",
].map((item, idx) => (
<div key={idx} className="flex items-start gap-2">
<span className="text-teal-500"></span>
<p>{item}</p>
</div>
))}
</div>
<div className="max-w-7xl relative my-5 h-[125px] overflow-hidden flex items-center mx-auto border">
<Image
src="/image-kolom.png"
alt="Berita Utama"
fill
className="object-cover"
/>
</div>
</section>
);
};
export default TravelNews;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,125 @@
"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 - 1,
endPage + (5 - (endPage - startPage + 1))
);
} else if (page + halfWindow >= totalPage) {
startPage = Math.max(2, startPage - (5 - (endPage - startPage + 1)));
}
}
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(
<PaginationItem key={i} onClick={() => setPage(i)}>
<PaginationLink className="cursor-pointer" isActive={page === i}>
{i}
</PaginationLink>
</PaginationItem>
);
}
return pageNumbers;
};
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => (page > 10 ? setPage(page - 10) : "")}
>
{/* <DoubleArrowLeftIcon /> */}
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationPrevious
className="cursor-pointer"
onClick={() => (page > 1 ? setPage(page - 1) : "")}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(1)}
isActive={page === 1}
>
{1}
</PaginationLink>
</PaginationItem>
{page > 4 && (
<PaginationItem>
<PaginationEllipsis
className="cursor-pointer"
onClick={() => setPage(page - 1)}
/>
</PaginationItem>
)}
{renderPageNumbers()}
{page < totalPage - 3 && (
<PaginationItem>
<PaginationEllipsis
className="cursor-pointer"
onClick={() => setPage(page + 1)}
/>
</PaginationItem>
)}
{totalPage > 1 && (
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(totalPage)}
isActive={page === totalPage}
>
{totalPage}
</PaginationLink>
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
className="cursor-pointer"
onClick={() => (page < totalPage ? setPage(page + 1) : "")}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
onClick={() => (page < totalPage - 10 ? setPage(page + 10) : "")}
>
{/* <DoubleArrowRightIcon /> */}
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}

View File

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

View File

@ -0,0 +1,67 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setThemeState] = useState<Theme>('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
// Get theme from localStorage or default to 'light'
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
setThemeState(savedTheme);
} else {
// Check system preference
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setThemeState(systemTheme);
}
}, []);
useEffect(() => {
if (!mounted) return;
// Update document class and localStorage
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme, mounted]);
const toggleTheme = () => {
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
// Prevent hydration mismatch
if (!mounted) {
return <>{children}</>;
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View File

@ -0,0 +1,158 @@
"use client";
import { getStatisticMonthly } from "@/service/article";
import React, { useEffect, useState } from "react";
import { SafeReactApexChart } from "@/utils/dynamic-import";
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[]>([]);
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">
<SafeReactApexChart
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;

View File

@ -0,0 +1,396 @@
"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 {
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 "react-datepicker/dist/react-datepicker.css";
import { Checkbox } from "@/components/ui/checkbox";
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
import CustomPagination from "@/components/layout/custom-pagination";
import { motion } from "framer-motion";
import { Article } from "@/types/globals";
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="space-y-8">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
{/* User Profile Card */}
<motion.div
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">{fullname}</h3>
<p className="text-slate-600">{username}</p>
<div className="flex space-x-6 pt-2">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">
{summary?.totalToday}
</p>
<p className="text-sm text-slate-500">Today</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">
{summary?.totalThisWeek}
</p>
<p className="text-sm text-slate-500">This Week</p>
</div>
</div>
</div>
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
<DashboardUserIcon size={60} />
</div>
</div>
</motion.div>
{/* Total Posts */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl">
<DashboardSpeecIcon />
</div>
<div>
<p className="text-3xl font-bold text-slate-800">
{summary?.totalAll}
</p>
<p className="text-sm text-slate-500">Total Posts</p>
</div>
</div>
</motion.div>
{/* Total Views */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl">
<DashboardConnectIcon />
</div>
<div>
<p className="text-3xl font-bold text-slate-800">
{summary?.totalViews}
</p>
<p className="text-sm text-slate-500">Total Views</p>
</div>
</div>
</motion.div>
{/* Total Shares */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
<DashboardShareIcon />
</div>
<div>
<p className="text-3xl font-bold text-slate-800">
{summary?.totalShares}
</p>
<p className="text-sm text-slate-500">Total Shares</p>
</div>
</div>
</motion.div>
{/* Total Comments */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl">
<DashboardCommentIcon size={40} />
</div>
<div>
<p className="text-3xl font-bold text-slate-800">
{summary?.totalComments}
</p>
<p className="text-sm text-slate-500">Total Comments</p>
</div>
</div>
</motion.div>
</div>
{/* Content Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Analytics Chart */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">
Analytics Overview
</h3>
<div className="flex space-x-4">
{options.map((option) => (
<label
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
checked={analyticsView.includes(option.value)}
onCheckedChange={(checked) =>
handleChange(option.value, checked as boolean)
}
/>
<span className="text-sm text-slate-600">{option.label}</span>
</label>
))}
</div>
</div>
<div className="h-80">
<ApexChartColumn
type="monthly"
date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`}
view={analyticsView}
/>
</div>
</motion.div>
{/* Recent Articles */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">
Recent Articles
</h3>
<Link href="/admin/article/create">
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
Create Article
</Button>
</Link>
</div>
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
{article?.map((list: any) => (
<motion.div
key={list?.id}
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300 }}
>
<Image
alt="thumbnail"
src={list?.thumbnailUrl || `/default-image.jpg`}
width={80}
height={80}
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
{list?.title}
</h4>
<p className="text-sm text-slate-500">
{convertDateFormat(list?.createdAt)}
</p>
</div>
</motion.div>
))}
</div>
<div className="mt-6 flex justify-center">
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</motion.div>
</div>
</div>
);
}

View File

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

View File

@ -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 {
createMediaFileAdvertise,
deleteAdvertise,
editAdvertise,
editAdvertiseIsActive,
getAdvertise,
getAdvertiseById,
} 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 getAdvertiseById(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>
</>
);
}

View File

@ -0,0 +1,448 @@
"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,
getArticlePagination,
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();
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,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
sortBy: "created_at",
};
const res = await getArticlePagination(req);
await getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
close();
}
// panggil ulang setiap state berubah
useEffect(() => {
initState();
}, [page, showData, search, selectedCategories]);
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>
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

46
components/ui/badge.tsx Normal file
View File

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

View File

@ -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,
}

59
components/ui/button.tsx Normal file
View File

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

213
components/ui/calendar.tsx Normal file
View File

@ -0,0 +1,213 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

92
components/ui/card.tsx Normal file
View File

@ -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,
}

View File

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

23
components/ui/chip.tsx Normal file
View File

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

143
components/ui/dialog.tsx Normal file
View File

@ -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,
}

View File

@ -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,
}

21
components/ui/input.tsx Normal file
View File

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

24
components/ui/label.tsx Normal file
View File

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

View File

@ -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,
}

48
components/ui/popover.tsx Normal file
View File

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

View File

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

View File

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

185
components/ui/select.tsx Normal file
View File

@ -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,
}

31
components/ui/switch.tsx Normal file
View File

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

116
components/ui/table.tsx Normal file
View File

@ -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,
}

View File

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

View File

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

92
config/swal.ts Normal file
View File

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

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

16
next.config.ts Normal file
View File

@ -0,0 +1,16 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
domains: ["mikulnews.com", "dev.mikulnews.com"],
},
eslint: {
ignoreDuringBuilds: true,
},
// Add experimental features for better chunk handling
experimental: {
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
},
};
export default nextConfig;

5605
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

61
package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "milenial-bersuara",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start"
},
"dependencies": {
"@ckeditor/ckeditor5-react": "^11.0.0",
"@ckeditor/ckeditor5-watchdog": "^46.0.2",
"@hookform/resolvers": "^5.2.1",
"@iconify/iconify": "^3.1.1",
"@iconify/react": "^6.0.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@types/js-cookie": "^3.0.6",
"apexcharts": "^5.3.4",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.2.6",
"framer-motion": "^12.23.12",
"js-cookie": "^3.0.5",
"lucide-react": "^0.542.0",
"next": "15.5.2",
"react": "19.1.0",
"react-apexcharts": "^1.7.0",
"react-datepicker": "^8.7.0",
"react-day-picker": "^9.9.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.59.0",
"react-password-checklist": "^1.8.1",
"react-select": "^5.10.2",
"sweetalert2": "^11.22.5",
"sweetalert2-react-content": "^5.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.67"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.7",
"typescript": "^5"
}
}

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
public/advertisiment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

BIN
public/arus-mudik.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
public/blogger.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

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