feat:dashboard, admin setting, fixing thumbnail

This commit is contained in:
Rama Priyanto 2025-01-23 22:30:33 +07:00
parent a738e61277
commit 1b8b4e4302
23 changed files with 1435 additions and 627 deletions

View File

@ -7,19 +7,6 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
export default function BasicPage() {
const router = useRouter();
const setGeneratedArticleIdStore = generatedArticleIds(
(state) => state.setArticleIds
);
const goGenerate = () => {
setGeneratedArticleIdStore({
singleArticle: [],
bulkArticle: [],
rewriteArticle: [],
});
router.push("/admin/article/generate");
};
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">

View File

@ -1,15 +1,25 @@
"use client";
import PasswordForm from "@/components/form/settings/password";
import ProfileForm from "@/components/form/settings/profile";
import { getProfile } from "@/service/master-user";
import { Tab, Tabs } from "@nextui-org/react";
import { useEffect, useState } from "react";
export default function Settings() {
const [profile, setProfile] = useState<any>();
useEffect(() => {
const initFetch = async () => {
const profile = await getProfile();
setProfile(profile?.data?.data);
};
initFetch();
}, []);
return (
<div className="w-full lg:w-[60%] p-5">
<div className="flex flex-col bg-gray-100 dark:bg-stone-900 text-black dark:text-white rounded-md p-5">
<Tabs aria-label="Tabs radius" radius="sm">
<Tab key="profile" title="Profile">
<ProfileForm />
<ProfileForm profile={profile} />
</Tab>
<Tab key="music" title="Password">
<PasswordForm />

View File

@ -1,5 +1,3 @@
import { HumasLayout } from "@/components/layout/humas-layout";
export default function AuthLayout({
children,
}: {

View File

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

191
app/setup-password/page.tsx Normal file
View File

@ -0,0 +1,191 @@
"use client";
import { Input } from "@nextui-org/input";
import React, { useState } from "react";
import { Button } from "@nextui-org/button";
import Link from "next/link";
import Cookies from "js-cookie";
import { close, error, loading } from "@/config/swal";
import { getProfile, postSignIn } from "@/service/master-user";
import { useRouter, useSearchParams } from "next/navigation";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { EyeFilledIcon, EyeSlashFilledIcon } from "@/components/icons";
import PasswordChecklist from "react-password-checklist";
export default function Login() {
const router = useRouter();
const [isVisible, setIsVisible] = useState([false, false]);
const searchParams = useSearchParams();
const userId = searchParams.get("id");
const [passwordConf, setPasswordConf] = useState("");
const [password, setPassword] = useState("");
const [isValidPassword, setIsValidPassword] = useState(false);
const onSubmit = async () => {
const data = {
userId: String(userId),
password: password,
};
console.log("data", data);
};
const MySwal = withReactContent(Swal);
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 (
<div className="flex flex-row h-full">
<div
style={{
backgroundImage: "url(headerbanner1.png)",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition: "left center",
}}
className="h-screen hidden md:block md:w-3/5"
>
<Link href="/">
<img src="divhumas.png" className="w-[120px]" />
</Link>
</div>
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
<p className="text-3xl md:text-[50px] text-[#DD8306] font-semibold mb-10">
Buat Password Baru
</p>
<p className="my-2 text-white">Password</p>
<Input
isRequired
type={isVisible[0] ? "text" : "password"}
label=""
placeholder=""
className="my-2"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={() => setIsVisible([!isVisible[0], isVisible[1]])}
>
{isVisible[0] ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
value={password}
onChange={(e: any) => {
setPassword(e.target.value.trim());
}}
onPaste={(e: any) => {
setPassword(e.target.value.trim());
}}
onCopy={(e: any) => {
setPassword(e.target.value.trim());
}}
/>
<p className="text-white my-2">Konfirmasi Password</p>
<Input
isRequired
className="my-2"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={() => setIsVisible([isVisible[0], !isVisible[1]])}
>
{isVisible[1] ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
type={isVisible[1] ? "text" : "password"}
label=""
placeholder=""
value={passwordConf}
onChange={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
onPaste={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
onCopy={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
/>
<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-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
size="lg"
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white"
onPress={onSubmit}
isDisabled={!isValidPassword}
>
Simpan
</Button>
<div className="flex justify-between my-2 text-white">
<Link href={`/auth`} className="text-[#DD8306] cursor-pointer">
Login
</Link>
{/* <div>
<Checkbox color="warning"></Checkbox> Remember me
</div> */}
</div>
</div>
</div>
);
}

View File

@ -94,7 +94,10 @@ export default function CreateArticleForm() {
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [tag, setTag] = useState("");
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number>();
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null
);
const [thumbnailValidation, setThumbnailValidation] = useState("");
const [diseData, setDiseData] = useState<DiseData>();
const { getRootProps, getInputProps } = useDropzone({
@ -148,19 +151,24 @@ export default function CreateArticleForm() {
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(values);
}
});
if (thumbnailImg.length < 1 && !selectedMainImage) {
setThumbnailValidation("Required");
} else {
setThumbnailValidation("");
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(values);
}
});
}
};
useEffect(() => {
@ -265,11 +273,8 @@ export default function CreateArticleForm() {
const formFiles = new FormData();
if (selectedMainImage) {
formFiles.append("files", files[selectedMainImage]);
formFiles.append("files", files[selectedMainImage - 1]);
const resFile = await uploadArticleThumbnail(articleId, formFiles);
} else {
formFiles.append("files", files[0]);
const resFile = await uploadArticleThumbnail(articleId, formFiles);
}
}
@ -348,8 +353,8 @@ export default function CreateArticleForm() {
id={String(index)}
value={String(index)}
size="sm"
isSelected={selectedMainImage === index}
onValueChange={() => setSelectedMainImage(index)}
isSelected={selectedMainImage === index + 1}
onValueChange={() => setSelectedMainImage(index + 1)}
>
<p className="text-black text-xs"> Jadikan Thumbnail</p>
</Checkbox>
@ -395,6 +400,14 @@ export default function CreateArticleForm() {
setValue("tags", uniqueArray as [string, ...string[]]);
};
useEffect(() => {
console.log("seklec", selectedMainImage);
console.log("seklssssec", files);
if (selectedMainImage) {
console.log("filll", files[selectedMainImage]);
}
}, [selectedMainImage]);
return (
<form
className="flex flex-row gap-8 text-black"
@ -518,7 +531,24 @@ export default function CreateArticleForm() {
<div className="h-fit bg-white rounded-lg p-8 flex flex-col gap-1">
<p className="text-sm">Thubmnail</p>
{thumbnailImg.length > 0 ? (
{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="bordered"
size="sm"
color="danger"
onClick={() => setSelectedMainImage(null)}
>
<TimesIcon />
</Button>
</div>
) : thumbnailImg.length > 0 ? (
<div className="flex flex-row">
<img
src={URL.createObjectURL(thumbnailImg[0])}
@ -537,19 +567,19 @@ export default function CreateArticleForm() {
</div>
) : (
<>
<label htmlFor="file-upload">
<Button className="w-fit" color="primary" size="sm" as="span">
Upload Thumbnail
</Button>
</label>{" "}
{/* <label htmlFor="file-upload">
<button>Upload Thumbnail</button>
</label>{" "} */}
<input
id="file-upload"
type="file"
multiple
style={{ display: "none" }}
className="w-fit h-fit"
onChange={handleFileChange}
/>
{thumbnailValidation !== "" && (
<p className="text-red-400 text-sm mb-3">Thumbnail harus ada</p>
)}
</>
)}

View File

@ -20,6 +20,7 @@ import {
getArticleById,
updateArticle,
uploadArticleFile,
uploadArticleThumbnail,
} from "@/service/article";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
@ -106,6 +107,11 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
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 [thumbnailValidation, setThumbnailValidation] = useState("");
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
@ -141,10 +147,8 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
async function initState() {
loading();
const res = await getArticleById(id);
// setArticle(data);
const data = res.data?.data;
setValue("title", data?.title);
// setTypeId(String(data?.typeId));
setValue("slug", data?.slug);
setValue("description", data?.htmlDescription);
setValue("tags", data?.tags ? data.tags.split(",") : []);
@ -161,8 +165,9 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
const temp: CategoryType[] = [];
for (let i = 0; i < data.length; i++) {
const datas = listCategory.filter((a) => a.id == data[i].id);
temp.push(datas[0]);
if (datas[0]) {
temp.push(datas[0]); // Hanya tambahkan jika datas[0] ada
}
}
setValue("category", temp as [CategoryType, ...CategoryType[]]);
};
@ -234,6 +239,13 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
}
}
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadArticleThumbnail(String(id), formFiles);
}
close();
successSubmit("/admin/article");
};
@ -351,6 +363,13 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
});
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files;
if (selectedFiles) {
setThumbnailImg(Array.from(selectedFiles));
}
};
return (
<form
className="flex flex-row gap-8 text-black"
@ -550,6 +569,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
<Button
className=" border-none rounded-full"
variant="bordered"
color="danger"
onClick={() => handleDeleteFile(file?.id)}
>
<TimesIcon />
@ -563,7 +583,70 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
<div className="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>
<img src={thumbnail} className="w-[30%]" alt="thumbnail" />
{isDetail ? (
<img src={thumbnail} className="w-[30%]" alt="thumbnail" />
) : 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="bordered"
size="sm"
color="danger"
onClick={() => setSelectedMainImage(null)}
>
<TimesIcon />
</Button>
</div>
) : thumbnail !== "" ? (
<div className="flex flex-row">
<img src={thumbnail} className="w-[30%]" alt="thumbnail" />
<Button
className=" border-none rounded-full"
variant="bordered"
size="sm"
color="danger"
onClick={() => setThumbnail("")}
>
<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="bordered"
size="sm"
color="danger"
onClick={() => setThumbnailImg([])}
>
<TimesIcon />
</Button>
</div>
) : (
<>
<input
id="file-upload"
type="file"
multiple
className="w-fit h-fit"
onChange={handleFileChange}
/>
{thumbnailValidation !== "" && (
<p className="text-red-400 text-sm mb-3">Thumbnail harus ada</p>
)}
</>
)}
<p className="text-sm mt-3">Kategori</p>
<Controller
control={control}

View File

@ -8,8 +8,8 @@ import Cookies from "js-cookie";
import { close, error, loading } from "@/config/swal";
import { getProfile, postSignIn } from "@/service/master-user";
import { useRouter } from "next/navigation";
import { HumasLayout } from "../layout/humas-layout";
import { Checkbox } from "@nextui-org/react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
export default function Login() {
const router = useRouter();
@ -52,7 +52,7 @@ export default function Login() {
secure: true,
sameSite: "strict",
});
const profile = await getProfile();
const profile = await getProfile(access_token);
console.log("PROFILE : ", profile?.data);
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
@ -100,8 +100,38 @@ export default function Login() {
setUsername(uname.toLowerCase());
};
const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal);
const checkUsername = async () => {
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) {
}
});
};
return (
<div className="flex flex-row h-screen">
<div className="flex flex-row h-full">
<div
style={{
backgroundImage: "url(headerbanner1.png)",
@ -115,69 +145,128 @@ export default function Login() {
<img src="divhumas.png" className="w-[120px]" />
</Link>
</div>
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 md:py-36 flex flex-col">
<p className="text-[72px] text-[#DD8306] font-semibold mb-10">Login</p>
<p className="my-2 text-white">Username</p>
<Input
isRequired
type="text"
label=""
placeholder=""
className="my-2"
onChange={(e: any) => {
setValUsername(e.target.value.trim());
}}
onPaste={(e: any) => {
setValUsername(e.target.value.trim());
}}
onCopy={(e: any) => {
setValUsername(e.target.value.trim());
}}
/>
<p className="text-white my-2">Password</p>
<Input
isRequired
className="my-2"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
>
{isVisible ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
type={isVisible ? "text" : "password"}
label=""
placeholder=""
onChange={(event) => setPassword(event.target.value)}
/>
<div className="flex justify-between my-8 text-white">
<div>
<Checkbox color="warning"></Checkbox> Remember me
</div>
<Link href="#" className="text-[#DD8306]">
{isResetPassword ? (
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
<p className="text-[72px] text-[#DD8306] font-semibold mb-10">
Reset Password
</Link>
</p>
<p className="my-2 text-white">Username</p>
<Input
isRequired
type="text"
label=""
placeholder=""
className="my-2"
value={checkUsernameValue}
onChange={(e: any) => {
setCheckUsernameValue(e.target.value.trim());
}}
onPaste={(e: any) => {
setCheckUsernameValue(e.target.value.trim());
}}
onCopy={(e: any) => {
setCheckUsernameValue(e.target.value.trim());
}}
/>
<Button
size="lg"
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white"
onPress={checkUsername}
isDisabled={checkUsernameValue == ""}
>
Check Username
</Button>
<div className="flex justify-between md:justify-end my-2 text-white">
<Link
href={`/`}
className="text-[#DD8306] cursor-pointer md:hidden"
>
Beranda
</Link>
{/* <div>
<Checkbox color="warning"></Checkbox> Remember me
</div> */}
<a
className="text-[#DD8306] cursor-pointer"
onClick={() => setIsResetPassword(false)}
>
Login
</a>
</div>
</div>
<Button
size="lg"
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white"
onPress={onSubmit}
>
Login
</Button>
<div className="text-center text-sm">
Don&apos;t have accout yet?{" "}
<Link href="#" className="text-[#DD8306]">
New Account
</Link>
) : (
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
<p className="text-[72px] text-[#DD8306] font-semibold mb-10">
Login
</p>
<p className="my-2 text-white">Username</p>
<Input
isRequired
type="text"
label=""
placeholder=""
className="my-2"
value={username}
onChange={(e: any) => {
setValUsername(e.target.value.trim());
}}
onPaste={(e: any) => {
setValUsername(e.target.value.trim());
}}
onCopy={(e: any) => {
setValUsername(e.target.value.trim());
}}
/>
<p className="text-white my-2">Password</p>
<Input
isRequired
className="my-2"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
>
{isVisible ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
type={isVisible ? "text" : "password"}
label=""
placeholder=""
onChange={(event) => setPassword(event.target.value)}
/>
<Button
size="lg"
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white"
onPress={onSubmit}
>
Login
</Button>
<div className="flex justify-between md:justify-end my-2 text-white">
<Link
href={`/`}
className="text-[#DD8306] cursor-pointer md:hidden"
>
Beranda
</Link>
{/* <div>
<Checkbox color="warning"></Checkbox> Remember me
</div> */}
<a
className="text-[#DD8306] cursor-pointer"
onClick={() => setIsResetPassword(true)}
>
Reset Password
</a>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -33,7 +33,11 @@ import {
PptIcon,
WordIcon,
} from "@/components/icons/globals";
import { createMagazine, uploadMagazineFile } from "@/service/magazine";
import {
createMagazine,
uploadMagazineFile,
uploadMagazineThumbnail,
} from "@/service/magazine";
const CustomEditor = dynamic(
() => {
@ -85,6 +89,7 @@ export default function NewCreateMagazineForm() {
const router = useRouter();
const editor = useRef(null);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
@ -105,8 +110,6 @@ export default function NewCreateMagazineForm() {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [
".xlsx",
],
"application/vnd.ms-excel": [".xls"],
"text/csv": [".csv"],
},
});
@ -172,6 +175,12 @@ export default function NewCreateMagazineForm() {
const resFile = await uploadMagazineFile(magazineId, formFiles);
}
}
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadMagazineThumbnail(magazineId, formFiles);
}
close();
successSubmit("/admin/magazine");
@ -308,6 +317,13 @@ export default function NewCreateMagazineForm() {
</div>
));
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files;
if (selectedFiles) {
setThumbnailImg(Array.from(selectedFiles));
}
};
return (
<form
className="flex flex-row gap-8 text-black"
@ -369,7 +385,36 @@ export default function NewCreateMagazineForm() {
{errors?.slug && (
<p className="text-red-400 text-sm mb-3">{errors.slug?.message}</p>
)}
<p className="text-sm mt-3">Thumbnail</p>
{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="bordered"
size="sm"
color="danger"
onClick={() => setThumbnailImg([])}
>
<TimesIcon />
</Button>
</div>
) : (
<>
<input
id="file-upload"
type="file"
multiple
className="w-fit h-fit"
onChange={handleFileChange}
/>
</>
)}
<p className="text-sm mt-3">Deskripsi</p>
<Controller
control={control}
@ -395,7 +440,7 @@ export default function NewCreateMagazineForm() {
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .doc, .docx, .pdf, .ppt, .pptx,
.xlsx, .csv maksimal 100mb.)
maksimal 100mb.)
</div>
</div>
</div>

View File

@ -40,6 +40,7 @@ import {
getMagazineById,
updateMagazine,
uploadMagazineFile,
uploadMagazineThumbnail,
} from "@/service/magazine";
const CustomEditor = dynamic(
@ -48,6 +49,12 @@ const CustomEditor = dynamic(
},
{ ssr: false }
);
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
);
interface FileWithPreview extends File {
preview: string;
@ -74,16 +81,18 @@ const createArticleSchema = z.object({
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
rows: z.array(
z.object({
title: z.string().min(1, {
message: "Main Keyword must be at least 2 characters.",
}),
description: z.string().min(1, {
message: "Title must be at least 2 characters.",
}),
})
),
rows: z
.array(
z.object({
title: z.string().min(1, {
message: "Main Keyword must be at least 2 characters.",
}),
description: z.string().min(1, {
message: "Title must be at least 2 characters.",
}),
})
)
.optional(),
});
export default function EditMagazineForm(props: { isDetail: boolean }) {
@ -95,6 +104,8 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
const router = useRouter();
const editor = useRef(null);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [prevThumbnail, setPrevThumbnail] = useState("");
const [detailfiles, setDetailFiles] = useState<any>([]);
const { getRootProps, getInputProps } = useDropzone({
@ -116,8 +127,6 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [
".xlsx",
],
"application/vnd.ms-excel": [".xls"],
"text/csv": [".csv"],
},
});
@ -147,6 +156,14 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
const data = res?.data?.data;
setValue("title", data?.title);
setValue("description", data?.description);
if (res?.data?.data?.thumbnailPath) {
setPrevThumbnail(
res?.data?.data?.thumbnailPath?.split("/")[
res?.data?.data?.thumbnailPath?.split("/").length - 1
]
);
}
setDetailFiles(data?.files);
console.log("datasss", data);
@ -191,13 +208,22 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
const formFiles = new FormData();
for (let i = 0; i < files.length; i++) {
const rows = values?.rows || [];
formFiles.append("files", files[i]);
formFiles.append("title", values.rows[i].title);
formFiles.append("file", values.rows[i].description);
formFiles.append("title", rows[i]?.title || "");
formFiles.append("file", rows[i]?.description || "");
const resFile = await uploadMagazineFile(String(id), formFiles);
}
}
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadMagazineThumbnail(String(id), formFiles);
}
close();
successSubmit("/admin/magazine");
};
@ -377,6 +403,13 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
});
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files;
if (selectedFiles) {
setThumbnailImg(Array.from(selectedFiles));
}
};
return (
<form
className="flex flex-row gap-8 text-black"
@ -440,13 +473,67 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
<p className="text-red-400 text-sm mb-3">{errors.slug?.message}</p>
)}
<p className="text-sm mt-3">Thumbnail</p>
{prevThumbnail !== "" ? (
<div className="flex flex-row">
<img
src={`http://38.47.180.165:8802/magazines/thumbnail/viewer/${prevThumbnail}`}
className="w-[30%]"
alt="thumbnail"
/>
{!isDetail && (
<Button
className=" border-none rounded-full"
variant="bordered"
size="sm"
color="danger"
onClick={() => setPrevThumbnail("")}
>
<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="bordered"
size="sm"
color="danger"
onClick={() => setThumbnailImg([])}
>
<TimesIcon />
</Button>
</div>
) : (
<>
<input
id="file-upload"
type="file"
multiple
className="w-fit h-fit"
onChange={handleFileChange}
/>
</>
)}
<p className="text-sm mt-3">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor onChange={onChange} initialData={value} />
)}
render={({ field: { onChange, value } }) =>
isDetail ? (
<ViewEditor initialData={value} />
) : (
<CustomEditor onChange={onChange} initialData={value} />
)
}
/>
{errors?.description && (
<p className="text-red-400 text-sm mb-3">
@ -466,7 +553,7 @@ export default function EditMagazineForm(props: { isDetail: boolean }) {
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .doc, .docx, .pdf, .ppt, .pptx,
.xlsx, .csv maksimal 100mb.)
maksimal 100mb.)
</div>
</div>
</div>

View File

@ -1,108 +1,174 @@
"use client";
import { FormEvent, Fragment, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { Input, Textarea } from "@nextui-org/input";
const formSchema = z.object({
password: z.string().min(2, {
message: "Judul harus diisi",
}),
passwordConf: z.string().min(2, {
message: "Slug harus diisi",
}),
});
import { Input } from "@nextui-org/input";
import { EyeFilledIcon, EyeSlashFilledIcon } from "@/components/icons";
import { Button } from "@nextui-org/button";
import PasswordChecklist from "react-password-checklist";
export default function PasswordForm() {
const MySwal = withReactContent(Swal);
const [isVisible, setIsVisible] = useState([false, false]);
const formOptions = {
resolver: zodResolver(formSchema),
const [passwordConf, setPasswordConf] = useState("");
const [password, setPassword] = useState("");
const [isValidPassword, setIsValidPassword] = useState(false);
const onSubmit = async () => {
const data = {
password: password,
};
console.log("data", data);
};
type UserSettingSchema = z.infer<typeof formSchema>;
const {
register,
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
watch,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
console.log("values");
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 " onSubmit={handleSubmit(onSubmit)}>
<form className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<p className="text-sm">Password</p>
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="password"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
<Input
isRequired
type={isVisible[0] ? "text" : "password"}
label=""
placeholder=""
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={() => setIsVisible([!isVisible[0], isVisible[1]])}
>
{isVisible[0] ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
value={password}
onChange={(e: any) => {
setPassword(e.target.value.trim());
}}
onPaste={(e: any) => {
setPassword(e.target.value.trim());
}}
onCopy={(e: any) => {
setPassword(e.target.value.trim());
}}
/>
{errors?.password && (
<p className="text-red-400 text-sm mb-3">
{errors.password?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Konfirmasi Password</p>
<Controller
control={control}
name="passwordConf"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="passwordConf"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
<Input
isRequired
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={() => setIsVisible([isVisible[0], !isVisible[1]])}
>
{isVisible[1] ? (
<EyeSlashFilledIcon className="text-2xl text-default-400 pointer-events-none" />
) : (
<EyeFilledIcon className="text-2xl text-default-400 pointer-events-none" />
)}
</button>
}
type={isVisible[1] ? "text" : "password"}
label=""
placeholder=""
value={passwordConf}
onChange={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
onPaste={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
onCopy={(e: any) => {
setPasswordConf(e.target.value.trim());
}}
/>
{errors?.passwordConf && (
<p className="text-red-400 text-sm mb-3">
{errors.passwordConf?.message}
</p>
)}
</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-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"
color="primary"
onPress={onSubmit}
isDisabled={!isValidPassword}
>
Simpan
</Button>
</form>
);
}

View File

@ -27,6 +27,9 @@ const formSchema = z.object({
nrp: z.string().min(1, {
message: "Harus diisi",
}),
address: z.string().min(1, {
message: "Harus diisi",
}),
gender: z.string().min(1, {
message: "Harus diisi",
}),
@ -35,9 +38,9 @@ const formSchema = z.object({
}),
});
export default function ProfileForm() {
export default function ProfileForm(props: { profile: any }) {
const MySwal = withReactContent(Swal);
const { profile } = props;
const formOptions = {
resolver: zodResolver(formSchema),
};
@ -54,8 +57,18 @@ export default function ProfileForm() {
clearErrors,
} = 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>) => {
console.log("values");
console.log("values", values);
};
return (
@ -183,6 +196,35 @@ export default function ProfileForm() {
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Alamat</p>
<Controller
control={control}
name="address"
render={({ field: { onChange, value } }) => (
<Textarea
type="text"
id="address"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
/>
{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
@ -204,7 +246,38 @@ export default function ProfileForm() {
<p className="text-red-400 text-sm mb-3">{errors.gender?.message}</p>
)}
</div>
<Button color="primary" type="submit">
<div className="flex flex-col gap-1">
<p className="text-sm">No Handphone</p>
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange, value } }) => (
<Input
type="number"
id="phoneNumber"
placeholder=""
label=""
value={value}
onChange={onChange}
labelPlacement="outside"
className="w-full "
classNames={{
inputWrapper: [
"border-1 rounded-lg",
"dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
],
}}
variant="bordered"
/>
)}
/>
{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

@ -1,172 +1,184 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const DashboardUserIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
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="#ddbaff" d="M24,44L24,44c-8.216,0-15.137-6.14-16.116-14.297l-0.797-4.639C5.871,14.924,13.788,6,24,6h0 c10.212,0,18.129,8.924,16.912,19.063l-0.797,4.639C39.137,37.86,32.216,44,24,44z" /><path fill="#6c19ff" d="M37.701,10.916c-0.825-1.117-1.787-2.133-2.858-3.017C31.912,5.474,28.145,4.003,24,4.003 c-4.145,0-7.912,1.471-10.844,3.895c-0.554,0.458-1.084,0.951-1.58,1.485c-3.115,3.323-4.903,7.879-4.573,12.777 c3.362-1.449,5.88-4.482,6.615-8.158h20.764c0.735,3.677,3.253,6.709,6.615,8.158C41.278,17.982,40.019,14.053,37.701,10.916z" /><path fill="#ddbaff" d="M40,31H8c-1.657,0-3-1.343-3-3s1.343-3,3-3h32c1.657,0,3,1.343,3,3S41.657,31,40,31z" /><path fill="#2100c4" d="M37.701,13.913c-0.825-1.117-1.787-2.133-2.858-3.017C31.912,8.471,28.145,7,24,7 c-4.145,0-7.912,1.471-10.844,3.895c-0.554,0.458-1.084,0.951-1.58,1.485c-3.115,3.323-4.903,7.879-4.573,12.777 c3.362-1.449,5.88-4.482,6.615-8.158h20.764c0.735,3.677,3.253,6.709,6.615,8.158C41.278,20.979,40.019,17.05,37.701,13.913z" />
</svg>
<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
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>
<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
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="#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>
<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 DashboardBookmarkIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
export const DashboardArticle = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<polygon fill="#f55376" points="37,6 11,6 11,30 24,36 37,42" />
<polygon fill="#f55376" points="11,6 37,6 37,30 24,36 11,42" />
<polygon fill="#eb0000" points="11,6 11,30 24,36 31.5,32.538 37,25 37,6" />
<g>
<polygon fill="#fadb00" points="37,29.768 48,21 26,21" />
<polygon fill="#fadb00" points="37,29.732 43.957,34 44,34 37,14 30,34 30.044,34" />
<polygon fill="#f5bc00" points="39.45,21 34.55,21 32.685,26.329 36.974,29.748 37,29.732 37.026,29.748 41.315,26.329" />
</g>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M3 17a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2zM15 5h-5V4h5zm0 2h-5V6h5zm0 2h-5V8h5zM5 14h10v1H5zm0-2h10v1H5zm0-2h10v1H5zm0-6h4v5H5z"
/>
</svg>
);
export const DashboardSpeecIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<circle cx="20" cy="28" r="16" fill="#3ddab4" />
<circle cx="31.584" cy="17.478" r="13.449" fill="#f5bc00" />
<polygon fill="#3ddab4" points="4.07,44 19.909,44 10.922,35.549" />
<path fill="#00b569" d="M20,12c-0.239,0-0.471,0.025-0.708,0.036c-0.739,1.665-1.157,3.504-1.157,5.443 c0,7.428,6.021,13.449,13.449,13.449c1.484,0,2.907-0.25,4.242-0.693C35.929,29.502,36,28.76,36,28C36,19.163,28.837,12,20,12z" />
<polygon fill="#f5bc00" points="44.975,4.029 31.661,4.029 39.215,11.133" />
</svg>
<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
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="#3dd9eb" d="M11,18c-3.313,0-6,2.686-6,6c0,3.313,2.687,6,6,6c3.314,0,6-2.687,6-6C17,20.687,14.314,18,11,18" />
<path fill="#3dd9eb" d="M37,5c-3.313,0-6,2.687-6,6c0,3.313,2.687,6,6,6c3.314,0,6-2.687,6-6C43,7.687,40.314,5,37,5" />
<path fill="#3dd9eb" d="M37,31c-3.313,0-6,2.686-6,6c0,3.313,2.687,6,6,6c3.314,0,6-2.687,6-6C43,33.687,40.314,31,37,31" />
<path fill="#6c19ff" d="M31.819,14.028L25.073,22h-8.415C16.88,22.626,17,23.299,17,24c0,0.701-0.12,1.374-0.341,2h8.414 l6.746,7.973c0.688-1.175,1.765-2.095,3.053-2.584L28.62,24l6.251-7.389C33.583,16.123,32.507,15.202,31.819,14.028" />
<path fill="#00b3d7" d="M16.658,22H11v4h5.659C16.88,25.375,17,24.701,17,24C17,23.299,16.88,22.626,16.658,22" />
<path fill="#00b3d7" d="M35.474,9.708l-3.655,4.32c0.688,1.175,1.764,2.095,3.053,2.584l3.655-4.319L35.474,9.708" />
<path fill="#00b3d7" d="M34.872,31.389c-1.288,0.489-2.365,1.409-3.053,2.584l3.655,4.319l3.053-2.584L34.872,31.389" />
</svg>
<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
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" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 18L6 6m0 0h9M6 6v9" />
</svg>
<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
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" fill-rule="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" clip-rule="evenodd" />
</svg>
);
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
fill-rule="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"
clip-rule="evenodd"
/>
</svg>
);

View File

@ -396,21 +396,12 @@ export const MasterCategoryIcon = ({
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 48 48"
viewBox="0 0 32 32"
{...props}
>
<defs>
<mask id="ipSTag0">
<g fill="none" strokeLinejoin="round" strokeWidth="4">
<path
fill="#fff"
stroke="#fff"
d="M8 44V6a2 2 0 0 1 2-2h28a2 2 0 0 1 2 2v38l-16-8.273z"
/>
<path stroke="#000" strokeLinecap="round" d="M16 18h16" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSTag0)" />
<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>
);

View File

@ -116,7 +116,7 @@ const sideBarDummyData = [
},
{
id: 30,
name: "Category",
name: "Kategori",
moduleId: 654,
moduleName: "Master",
modulePathUrl: "/admin/master-category",
@ -251,11 +251,11 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
const [sidebarMenu, setSidebarMenu] = useState<SidebarMenuTask[]>();
const { isOpen, toggleSidebar } = useSidebar();
const token = Cookies.get("access_token");
const fullname = Cookies.get("ufne");
const username = Cookies.get("username");
const isAuthenticated = Cookies.get("is_authenticated");
useEffect(() => {
if (!isAuthenticated) {
if (!token) {
onLogout();
}
}, [token]);
@ -445,7 +445,7 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
<div className="flex flex-row gap-3 items-center text-white">
<UserProfileIcon size={36} />
<div className="flex flex-col ">
<a className="cursor-pointer ">{fullname}</a>
<a className="cursor-pointer ">{username}</a>
<a
className="hover:text-red-600 underline text-sm cursor-pointer"
onClick={() => onLogout()}

View File

@ -1,81 +1,169 @@
import React, { Component } from 'react';
import ReactApexChart from 'react-apexcharts';
"use client";
import React, { Component, useEffect, useState } from "react";
import ReactApexChart from "react-apexcharts";
import dummyData from "../../../../const/dummy.json";
interface State {
series: { name: string; data: number[] }[];
options: {
chart: { type: string; height: number };
plotOptions: { bar: { colors: { ranges: { from: number; to: number; color: string }[] }; columnWidth: string } };
dataLabels: { enabled: boolean };
yaxis: { title: { text: string }; labels: { formatter: (y: number) => string } };
xaxis: { type: string; categories: string[]; labels: { rotate: number } };
};
type WeekData = {
week: number; // Minggu ke-
days: number[]; // Data jumlah view per hari dalam minggu tersebut
total: number; // Total jumlah view dalam minggu tersebut
};
// Struktur untuk sisa hari
type RemainingDays = {
days: number[]; // Data jumlah view untuk sisa hari
total: number; // Total jumlah view untuk sisa hari
};
// Struktur data per bulan setelah diolah
type ProcessedMonthlyData = {
id: string; // ID bulan
month: string; // Nama bulan
weeks: WeekData[]; // Data per minggu
remaining_days: RemainingDays; // Data sisa hari
};
// Struktur data input awal
type MonthlyDataInput = {
id: string; // ID bulan
month: string; // Nama bulan
count: number[]; // Jumlah view per hari selama 1 bulan
};
// Struktur array data awal
type MonthlyDataInputArray = MonthlyDataInput[];
function processMonthlyData(count: number[]): {
weeks: WeekData[];
remaining_days: RemainingDays;
} {
const weeks: WeekData[] = [];
let weekIndex = 1;
// Kelompokkan data per 7 hari (minggu)
for (let i = 0; i < count.length; i += 7) {
const weekData = count.slice(i, i + 7); // Ambil 7 hari
weeks.push({
week: weekIndex,
days: weekData,
total: weekData.reduce((sum, day) => sum + day, 0), // Total view per minggu
});
weekIndex++;
}
// Cek sisa hari
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,
};
}
class ApexChartColumn extends Component<{}, State> {
render() {
return (
<div>
<div id="chart">
<ReactApexChart
options={
{
chart: { type: 'bar', height: 350, zoom: { enabled: false } },
plotOptions: {
bar: {
colors: {
ranges: [
{
from: -100,
to: 0,
color: '#57c0fd'
},
{
from: 0,
to: 100,
color: '#6484fd',
const ApexChartColumn = (props: { type: string; date: string }) => {
const { date, type } = props;
const [categories, setCategories] = useState<string[]>([]);
const [series, setSeries] = useState<number[]>([]);
},
],
},
columnWidth: '20%',
},
},
dataLabels: {
enabled: false,
},
yaxis: {
useEffect(() => {
initFetch();
}, [props]);
labels: {
formatter: (y: any) => {
return y.toFixed(0);
},
},
},
xaxis: {
type: 'datetime',
categories: [
'2020-05-01', '2020-05-02', '2020-05-03', '2020-05-04', '2020-05-05',
],
labels: {
rotate: -90,
},
},
}}
series={[
{
name: 'Earning This Month ',
data: [
1.45, 5.9, -0.42, -12.6, -18.1
],
},
]} type="bar" height={350} />
</div>
<div id="html-dist"></div>
</div>
);
const initFetch = async () => {
const res = dummyData?.data;
const splitDate = date.split(" ");
const getDatas = res?.find(
(a) => a.month == splitDate[0] && a.year === splitDate[1]
);
if (getDatas) {
const temp = processMonthlyData(getDatas?.count);
setSeries(
temp.weeks.map((list) => {
return list.total;
})
);
if (type == "weekly") {
} else {
setSeries(getDatas.count);
}
if (type === "weekly") {
const category = [];
for (let i = 1; i <= temp.weeks.length; i++) {
category.push(`Week ${i}`);
}
setCategories(category);
}
} else {
setSeries([]);
}
}
};
export default ApexChartColumn;
const [state, setState] = useState({
series: [
{
name: "Visit",
data: [
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22,
24, 15, 18, 21, 26, 28, 23, 17, 20, 19, 22,
],
},
],
options: {
chart: {
height: 350,
type: "line",
zoom: {
enabled: false,
},
},
dataLabels: {
enabled: false,
},
stroke: {
curve: "straight",
},
grid: {
row: {
colors: ["#f3f3f3", "transparent"], // takes an array which will be repeated on columns
opacity: 0.5,
},
},
},
});
return (
<div>
<div id="chart">
<ReactApexChart
options={{
chart: {
height: 350,
type: "line",
zoom: {
enabled: false,
},
},
xaxis: {
categories: type == "weekly" ? categories : [],
},
}}
series={[
{
name: "Visit",
data: series,
},
]}
type="line"
height={350}
/>
</div>
<div id="html-dist"></div>
</div>
);
};
export default ApexChartColumn;

View File

@ -1,6 +1,6 @@
"use client";
import {
DashboardBookmarkIcon,
DashboardArticle,
DashboardBriefcaseIcon,
DashboardConnectIcon,
DashboardMailboxIcon,
@ -10,204 +10,207 @@ import {
DashboardUserIcon,
} from "@/components/icons/dashboard-icon";
import { Submenu1Icon } from "@/components/icons/sidebar-icon";
import { Button, Select, SelectItem, SelectSection } from "@nextui-org/react";
import {
Button,
Pagination,
Select,
SelectItem,
SelectSection,
} from "@nextui-org/react";
import ApexChartColumn from "./chart/column-chart";
import ApexChartDonut from "./chart/donut-chart";
import ApexChartLineArea from "./chart/line-area-chart";
import Cookies from "js-cookie";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getListArticle } from "@/service/article";
import { Article } from "@/types/globals";
import { convertDateFormat, textEllipsis } from "@/utils/global";
import Datepicker from "react-tailwindcss-datepicker";
type ArticleData = Article & {
no: number;
createdAt: string;
};
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 [article, setArticle] = useState<ArticleData[]>([]);
const [startDateValue, setStartDateValue] = useState({
startDate: new Date(),
endDate: new Date(),
});
const [typeDate, setTypeDate] = useState("monthly");
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);
}
const getMonthYear = (date: Date | string) => {
const newDate = new Date(date);
const months = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
const year = newDate.getFullYear();
const month = months[newDate.getMonth()];
return month + " " + year;
};
return (
<div className="p-8 flex justify-center">
<div className="w-full flex flex-col gap-6">
<div className="w-full flex flex-col md:flex-row gap-6 justify-center">
<div className="w-full md:w-[160px] h-[160px] bg-[#ecf2ff] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardUserIcon className="w-[50px]" />
<div className="px-8 py-4 justify-between w-full md:w-[35%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col rounded-lg">
<div className="flex justify-between w-full items-center">
<div className="flex flex-col gap-2">
<p className="font-bold text-xl ">{fullname}</p>
<p>{username}</p>
</div>
<DashboardUserIcon size={78} />
</div>
<div className="flex flex-row gap-5">
<p className="text-lg font-semibold">
4 <span className="text-sm font-normal">Artikel</span>
</p>
<p className="text-lg font-semibold">
2 <span className="text-sm font-normal">Majalah</span>
</p>
</div>
<div className="text-[#6484fd] font-semibold">Employees</div>
<div className="text-[#6484fd] font-semibold text-lg">96</div>
</div>
<div className="w-full md:w-[160px] h-[160px] bg-[#fef5e5] flex flex-col justify-center items-center rounded-lg">
<div className="w-full md:w-[25%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardBriefcaseIcon className="w-[50px]" />
<DashboardArticle />
</div>
<div className="text-[#feaf2d] font-semibold">Clients</div>
<div className="text-[#feaf2d] font-semibold text-lg">3650</div>
<div className="">Minggu ini</div>
<div className="font-semibold text-lg">24</div>
</div>
<div className="w-full md:w-[160px] h-[160px] bg-[#e8f7ff] flex flex-col justify-center items-center rounded-lg">
<div className="w-full md:w-[20%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardMailboxIcon className="w-[50px]" />
<DashboardSpeecIcon />
</div>
<div className="text-[#57c0fd] font-semibold">Projects</div>
<div className="text-[#57c0fd] font-semibold text-lg">356</div>
<div className="">Total post</div>
<div className="font-semibold text-lg">154</div>
</div>
<div className="w-full md:w-[160px] h-[160px] bg-[#fdede8] flex flex-col justify-center items-center rounded-lg">
<div className="w-full md:w-[20%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardBookmarkIcon className="w-[50px]" />
<DashboardConnectIcon />
</div>
<div className="text-[#f98e75] font-semibold">Events</div>
<div className="text-[#f98e75] font-semibold text-lg">696</div>
</div>
<div className="w-full md:w-[160px] h-[160px] bg-[#e6fffa] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardSpeecIcon className="w-[50px]" />
</div>
<div className="text-[#29e2be] font-semibold">Payroll</div>
<div className="text-[#29e2be] font-semibold text-lg">$96k</div>
</div>
<div className="w-full md:w-[160px] h-[160px] bg-[#ebf3fe] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardConnectIcon className="w-[50px]" />
</div>
<div className="text-[#5797fd] font-semibold">Reports</div>
<div className="text-[#5797fd] font-semibold text-lg">56</div>
<div className="">Total views</div>
<div className="font-semibold text-lg">530</div>
</div>
</div>
<div className="w-full flex flex-row gap-6 justify-center">
<div className="border-1 shadow-sm w-full rounded-lg md:w-[712px] p-6 flex flex-col">
<div className="w-full flex flex-row gap-6 justify-center ">
<div className="border-1 shadow-sm w-full rounded-lg md:w-[60%] p-6 flex flex-col">
<div className="flex justify-between mb-3">
<div className="font-semibold flex flex-col">
Revenue Update
<span className="font-normal text-xs text-gray-600">
Overview of Profit
</span>
Analytics
<span className="font-normal text-xs text-gray-600">Views</span>
</div>
<div className="w-auto md:w-[140px]">
<Select
label=""
labelPlacement="outside"
placeholder=""
className="w-full"
variant="bordered"
classNames={{
mainWrapper: "rounded",
listboxWrapper:
"bg-white w-full !text-indigo-500 text-center font-bold",
popoverContent: "bg-white !text-indigo-500",
trigger:
"border-1 border-gray-200 hover:!bg-gray-100 !text-black",
}}
listboxProps={{
itemClasses: {
base: [
"!text-left",
"!bg-white",
"text-indigo-500 ",
"data-[selectable=true]:!text-indigo-500",
"data-[pressed=true]:text-indigo-500",
"data-[hover=true]:!text-indigo-300",
],
wrapper: ["!bg-white border-none"],
},
}}
defaultSelectedKeys={["march"]}
// onChange={onChangeFilterStatus}
renderValue={(items) => {
return items.map((item) => (
<span
key={item.props?.value}
className="text-black text-xs"
>
{item.textValue}
</span>
));
}}
<div className="flex flex-row gap-2">
<Button
color="primary"
variant={typeDate === "monthly" ? "solid" : "bordered"}
onClick={() => setTypeDate("monthly")}
>
<SelectSection>
<SelectItem key="march">March 2023</SelectItem>
<SelectItem key="april">April 2023</SelectItem>
<SelectItem key="may">May 2023</SelectItem>
</SelectSection>
</Select>
Bulanan
</Button>
<Button
color="primary"
onClick={() => setTypeDate("weekly")}
variant={typeDate === "weekly" ? "solid" : "bordered"}
>
Mingguan
</Button>
<div className="w-auto md:w-[140px]">
<Datepicker
value={startDateValue}
displayFormat="DD/MM/YYYY"
asSingle={true}
useRange={false}
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>
<div className="flex flex-row w-full">
<div className="w-3/4">
<ApexChartColumn />
</div>
<div className="w-1/4 flex flex-col px-2 justify-between">
<div className="flex flex-row items-center gap-2">
<div className="bg-[#e8f7ff] p-2 rounded-md">
<Submenu1Icon className="h-full text-[#6484fd]" />
</div>
<div className="flex flex-col">
<p className="font-semibold text-xl">$63,489.50</p>
<p className="text-xs">Total Earnings</p>
</div>
</div>
<div className="flex flex-row gap-3 items-center">
<div className="w-2 h-2 bg-[#6484fd] rounded-full" />
<div className="flex flex-col">
<p className="text-xs">Earning this month</p>
<p className="font-semibold text-md">$48,820</p>
</div>
</div>
<div className="flex flex-row gap-3 items-center">
<div className="w-2 h-2 bg-[#57c0fd] rounded-full" />
<div className="flex flex-col">
<p className="text-xs">Expense this month</p>
<p className="font-semibold text-md">$26,498</p>
</div>
</div>
<Button
className="w-full h-[35px] rounded-md"
color="primary"
variant="solid"
>
View Full Report
</Button>
<div className="w-full">
<ApexChartColumn
type={typeDate}
date={getMonthYear(startDateValue.startDate)}
/>
</div>
</div>
</div>
<div className="flex flex-col w-[344px] gap-6">
<div className="h-1/2 border-1 shadow-sm w-full rounded-lg p-6 flex flex-row gap-2">
<div className="w-1/2 flex flex-col justify-between">
<p className="font-semibold">Yearly Breakup</p>
<div className="lflex flex-col">
<p className="font-semibold text-xl ">$36,358</p>
<div className="flex flex-row text-xs item-center">
<div className="rounded-full w-6 h-6 bg-[#e6fffa] p-[2px] flex items-center justify-center">
<DashboardTopLeftPointIcon className="text-[#29e2be]" />
</div>
<p className="pt-[3px]"> +9% last year</p>
</div>
</div>
<p className="font-semibold flex flex-row gap-3">
<div className="flex flex-row text-tiny items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[#5c6ac4]" />
2020
</div>
<div className="flex flex-row text-tiny items-center gap-2">
<div className="w-2 h-2 rounded-full bg-[#d4d4d8]" />
2020
</div>
</p>
</div>
<div className="w-1/2 h-full flex items-center">
<ApexChartDonut />
</div>
<div className="flex flex-col w-[40%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8">
<div className="flex justify-between">
<p>Recent Article</p>
<Link href="/admin/article/create">
<Button color="primary" variant="bordered">
Buat Article
</Button>
</Link>
</div>
<div className="h-1/2 border-1 shadow-sm w-full rounded-lg flex flex-col justify-between">
<div className="p-6 flex flex-col">
<div className="flex justify-between items-center font-semibold">
Monthly Earning
<a className="cursor-pointer w-10 h-10 bg-blue-300 flex justify-center items-center rounded-full text-white text-xl">
$
</a>
</div>
<div className="lflex flex-col">
<p className="font-semibold text-xl ">$6,820</p>
<div className="flex flex-row text-xs item-center">
<div className="rounded-full w-6 h-6 bg-[#fdede8] p-[2px] flex items-center justify-center">
<DashboardRightDownPointIcon className="text-[#f98e75]" />
</div>
<p className="pt-[3px]"> -6% last year</p>
</div>
{article?.map((list: any) => (
<div
key={list?.id}
className="flex flex-row gap-2 items-center border-b-2 py-2"
>
<img
src={list?.thumbnailUrl || `/no-image.jpg`}
className="h-[70px] w-[70px] object-cover rounded-lg"
/>
<div className="flex flex-col gap-2">
<p>{textEllipsis(list?.title, 40)}</p>
<p className="text-xs">
{convertDateFormat(list?.createdAt)}
</p>
</div>
</div>
<div className="w-auto object-cover">
<ApexChartLineArea />
</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)}
/>
</div>
</div>
</div>

View File

@ -269,7 +269,7 @@ export default function MagazineTable() {
</SelectItem>
</Select>
</div>
<div className="flex flex-col gap-1 w-[230px]">
{/* <div className="flex flex-col gap-1 w-[230px]">
<p className="font-semibold text-sm">Kategori</p>
<Select
label=""
@ -293,7 +293,7 @@ export default function MagazineTable() {
</SelectItem>
))}
</Select>
</div>
</div> */}
<div className="flex flex-col gap-1 w-full md:w-[240px]">
<p className="font-semibold text-sm">Tanggal</p>
<Datepicker

31
const/dummy.json Normal file
View File

@ -0,0 +1,31 @@
{
"data": [
{
"id": "1",
"year": "2024",
"month": "november",
"count": [
14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22
]
},
{
"id": "2",
"year": "2024",
"month": "december",
"count": [
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24,
15, 18, 21, 26, 28, 23, 17, 20, 19, 22, 22, 42, 32
]
},
{
"id": "3",
"year": "2025",
"month": "january",
"count": [
21, 24, 19, 27, 29, 23, 18, 20, 26, 22, 24, 30, 25, 19, 17, 21, 27, 23,
29, 25, 22
]
}
]
}

9
package-lock.json generated
View File

@ -54,6 +54,7 @@
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"react-password-checklist": "^1.8.1",
"react-select": "^5.8.3",
"react-sweetalert2": "^0.6.0",
"react-tailwindcss-datepicker": "^1.6.6",
@ -9090,6 +9091,14 @@
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-password-checklist": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/react-password-checklist/-/react-password-checklist-1.8.1.tgz",
"integrity": "sha512-QHIU/OejxoH4/cIfYLHaHLb+yYc8mtL0Vr4HTmULxQg3ZNdI9Ni/yYf7pwLBgsUh4sseKCV/GzzYHWpHqejTGw==",
"peerDependencies": {
"react": ">16.0.0-alpha || >17.0.0-alpha || >18.0.0-alpha"
}
},
"node_modules/react-property": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz",

View File

@ -55,6 +55,7 @@
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"react-password-checklist": "^1.8.1",
"react-select": "^5.8.3",
"react-sweetalert2": "^0.6.0",
"react-tailwindcss-datepicker": "^1.6.6",

View File

@ -57,6 +57,13 @@ export async function uploadMagazineFile(id: string, data: any) {
return await httpPost(`/magazine-files/${id}`, headers, data);
}
export async function uploadMagazineThumbnail(id: string, data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPost(`/magazines/thumbnail/${id}`, headers, data);
}
export async function deleteMagazineFiles(id: number) {
return await httpDeleteInterceptor(`magazine-files/${id}`);
}

View File

@ -34,10 +34,10 @@ export async function postSignIn(data: any) {
return await httpPost(pathUrl, headers, data);
}
export async function getProfile() {
export async function getProfile(code?: string) {
const headers = {
"content-type": "application/json",
Authorization: `Bearer ${token}`,
Authorization: `Bearer ${code || token}`,
};
return await httpGet(`/users/info`, headers);
}