feat:dashboard, admin setting, fixing thumbnail
This commit is contained in:
parent
a738e61277
commit
1b8b4e4302
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { HumasLayout } from "@/components/layout/humas-layout";
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <> {children}</>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue