first commit

This commit is contained in:
TuanVT
2026-05-23 09:48:07 +07:00
parent 03b9101bd5
commit b6ccde2c74
134 changed files with 7178 additions and 110 deletions
@@ -0,0 +1,8 @@
"use client";
import React from "react";
const MainPageHome = () => {
return <div>MainPageHome</div>;
};
export default MainPageHome;
@@ -0,0 +1 @@
export { default } from "./MainPageHome";
@@ -0,0 +1,66 @@
"use client";
import React, { useState } from "react";
import { IFormForgotPassword, TYPE_FORGOT_PASWORD } from "./interface";
import { ContextForgotPassword } from "./context";
import FormEmail from "./components/FormEmail";
import FormPassword from "./components/FormPassword/FormPassword";
export default function MainForgotPassword() {
const [type, setType] = useState<TYPE_FORGOT_PASWORD>(
TYPE_FORGOT_PASWORD.EMAIL,
);
const [form, setForm] = useState<IFormForgotPassword>({
email: "",
otp: "",
password: "",
rePassword: "",
});
return (
<div className="w-full">
{/* TITLE */}
<h3
className="
text-[34px]
font-bold
text-[#1A1B2D]
"
>
Quên mật khẩu
</h3>
{/* DESCRIPTION */}
<p
className="
mt-1
text-[14px]
font-medium
text-[#6F767E]
"
>
Nhập đa chỉ email liên kết với tài khoản của bạn đ lấy lại mật khẩu!
</p>
{/* FORM */}
<div className="mt-6">
<ContextForgotPassword.Provider
value={{
form,
setForm,
type,
setType,
}}
>
{type === TYPE_FORGOT_PASWORD.EMAIL && <FormEmail />}
{type === TYPE_FORGOT_PASWORD.PASSWORD && <FormPassword />}
</ContextForgotPassword.Provider>
</div>
</div>
);
}
@@ -0,0 +1,126 @@
"use client";
import React, { useContext } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { Mail } from "lucide-react";
import FormCustom from "@/components/utils/FormCustom";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import CustomPopup from "@/components/customs/custom-popup";
import CustomLoading from "@/components/customs/custom-loading";
import CustomButton from "@/components/customs/custom-button";
import { ContextForgotPassword } from "../../context";
import FormOTP from "../FormOTP";
import { PATH } from "@/constant/config";
import { httpRequest } from "@/services";
import accountServices from "@/services/accountServices";
function FormEmail() {
const router = useRouter();
const searchParams = useSearchParams();
const open = searchParams.get("_open");
const { form, setForm } = useContext(ContextForgotPassword);
const sendOTPMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Mã OTP đã được gửi đến email của bạn",
http: accountServices.sendOTP({
email: form?.email,
}),
});
},
onSuccess(data) {
if (!data) return;
const params = new URLSearchParams(searchParams.toString());
params.set("_open", "otp");
router.replace(`?${params.toString()}`);
},
});
const handleSendOTP = () => {
sendOTPMutation.mutate();
};
const handleClosePopup = () => {
const params = new URLSearchParams(searchParams.toString());
params.delete("_open");
router.replace(`?${params.toString()}`);
};
return (
<FormCustom form={form} setForm={setForm} onSubmit={handleSendOTP}>
<CustomLoading loading={sendOTPMutation.isPending} />
{/* INPUT EMAIL */}
<InputForm
label={
<span>
Email
<span className="text-red-500"> *</span>
</span>
}
placeholder="Nhập email"
type="text"
name="email"
isEmail
onClean
isRequired
isBlur
icon={<Mail size={22} />}
/>
{/* BUTTON GROUP */}
<div className="mt-6">
{/* BUTTON SUBMIT */}
<CustomButton
// type="submit"
disabled={sendOTPMutation.isPending}
variant="midnightBlue"
rounded="full"
className="w-full"
>
Lấy lại mật khẩu
</CustomButton>
{/* LINE */}
<div className="my-5 h-[1px] w-full bg-[#E5E5E5]" />
{/* LOGIN BUTTON */}
<CustomButton
// type="button"
variant="grey"
rounded="full"
className="w-full"
onClick={() => router.push(PATH.LOGIN)}
>
Đăng nhập ngay
</CustomButton>
</div>
{/* POPUP OTP */}
<CustomPopup open={open === "otp"} onClose={handleClosePopup}>
<FormOTP />
</CustomPopup>
</FormCustom>
);
}
export default FormEmail;
@@ -0,0 +1 @@
export { default } from "./FormEmail";
@@ -0,0 +1,241 @@
"use client";
import React, { useContext, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { X } from "lucide-react";
import { ContextForgotPassword } from "../../context";
import { TYPE_FORGOT_PASWORD } from "../../interface";
import CustomButton from "@/components/customs/custom-button";
import CustomLoading from "@/components/customs/custom-loading";
import { httpRequest } from "@/services";
import accountServices from "@/services/accountServices";
import InputSingle from "@/components/customs/custom-input";
import fancyTimeFormat, { obfuscateEmail } from "@/common/funcs/optionConvert";
function FormOTP() {
const TIME_OTP = 60;
const router = useRouter();
const searchParams = useSearchParams();
const [countDown, setCountDown] = useState<number>(TIME_OTP);
const { form, setForm, setType } = useContext(ContextForgotPassword);
/* ========================= COUNTDOWN ========================= */
useEffect(() => {
if (countDown <= 0) return;
const timeout = setTimeout(() => {
setCountDown((prev) => prev - 1);
}, 1000);
return () => clearTimeout(timeout);
}, [countDown]);
/* ========================= CLOSE POPUP ========================= */
const closeForm = () => {
const params = new URLSearchParams(searchParams.toString());
params.delete("_open");
router.replace(`?${params.toString()}`);
};
/* ========================= SEND OTP ========================= */
const sendOTPMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Mã OTP đã được gửi đến email của bạn",
http: accountServices.sendOTP({
email: form.email!,
}),
});
},
onSuccess(data) {
if (data) {
setCountDown(TIME_OTP);
}
},
});
/* ========================= VERIFY OTP ========================= */
const submitOTPMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Xác thực thành công",
http: accountServices.enterOTP({
email: form.email!,
otp: form.otp!,
}),
});
},
onSuccess(data) {
if (data) {
setType(TYPE_FORGOT_PASWORD.PASSWORD);
closeForm();
}
},
});
/* ========================= HANDLERS ========================= */
const handleSendCode = () => {
sendOTPMutation.mutate();
};
const handleSubmit = () => {
submitOTPMutation.mutate();
};
return (
<div
className="
relative
w-[476px]
rounded-2xl
bg-white
p-10
shadow-[0px_8px_40px_rgba(63,83,115,0.16),0px_3px_24px_rgba(63,83,115,0.2),0px_3px_8px_rgba(63,83,115,0.08),0px_1px_0px_rgba(63,83,115,0.08)]
"
>
{/* LOADING */}
<CustomLoading
loading={sendOTPMutation.isPending || submitOTPMutation.isPending}
/>
{/* TITLE */}
<h3
className="
text-center
text-[28px]
font-bold
text-[#1A1B2D]
"
>
Xác thực OTP
</h3>
{/* DESCRIPTION */}
<p
className="
mt-1
text-center
text-[14px]
font-medium
text-[#6F767E]
"
>
Một xác thực đã đưc gửi cho bạn qua đa chỉ email:
<span className="font-semibold"> {obfuscateEmail(form.email!)}</span>
</p>
{/* FORM */}
<div className="mt-6">
{/* LABEL */}
<p
className="
text-center
text-[16px]
font-medium
text-[#23262F]
"
>
Nhập OTP
</p>
{/* INPUT OTP */}
<div className="mt-4">
<InputSingle onSetValue={setForm} name="otp" lenght={6} />
</div>
{/* COUNTDOWN */}
<p
className="
mt-5
text-center
text-[15px]
font-medium
text-[#23262F]
"
>
Bạn chưa nhận đưc .
{countDown > 0 ? (
<span
className="
ml-1
cursor-default
text-[15px]
font-medium
text-[#0011AB]
"
>
Gửi lại OTP ({fancyTimeFormat(countDown)})
</span>
) : (
<span
onClick={handleSendCode}
className="
ml-1
cursor-pointer
text-[15px]
font-medium
text-[#0011AB]
transition-all
hover:underline
"
>
Gửi lại OTP
</span>
)}
</p>
</div>
{/* BUTTON */}
<div className="mt-6">
<CustomButton
variant="midnightBlue"
rounded="full"
className="w-full"
disabled={form.otp.length! < 6}
onClick={handleSubmit}
>
Xác thực Email
</CustomButton>
</div>
{/* CLOSE */}
<button
type="button"
onClick={closeForm}
className="
absolute
right-5
top-5
cursor-pointer
select-none
transition-all
active:scale-90
"
>
<X size={24} />
</button>
</div>
);
}
export default FormOTP;
@@ -0,0 +1 @@
export { default } from "./FormOTP";
@@ -0,0 +1,169 @@
"use client";
import React, { useContext } from "react";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { ShieldCheck } from "lucide-react";
import { useSelector } from "react-redux";
import md5 from "md5";
import { RootState, store } from "@/redux/store";
import {
setDataLoginStorage,
setStateLogin,
setToken,
} from "@/redux/reducer/auth";
import { setInfoUser } from "@/redux/reducer/user";
import { PATH } from "@/constant/config";
import { httpRequest } from "@/services";
import authServices from "@/services/authServices";
import { ContextForgotPassword, IContextForgotPassword } from "../../context";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import CustomButton from "@/components/customs/custom-button";
import CustomLoading from "@/components/customs/custom-loading";
import FormCustom from "@/components/utils/FormCustom";
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
export default function FormPassword() {
const router = useRouter();
const { dataLoginStorage } = useSelector((state: RootState) => state.auth);
const { isRememberPassword } = useSelector((state: RootState) => state.site);
const { form, setForm } = useContext<IContextForgotPassword>(
ContextForgotPassword,
);
const loginMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Đăng nhập thành công!",
http: authServices.login({
username: dataLoginStorage?.usernameStorage || "",
password: md5(
`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASSWORD}`,
),
ip: "",
address: "",
type: 0,
}),
});
},
onSuccess(data: any) {
if (!data) return;
store.dispatch(setStateLogin(true));
store.dispatch(setToken(data.accessToken));
store.dispatch(
setInfoUser({
accessToken: data?.accessToken || "",
refreshToken: data?.refreshToken || "",
accessExpiresAt: data?.accessExpiresAt || "",
refreshExpiresAt: data?.refreshExpiresAt || "",
avatar: data?.avatar || "",
fullname: data?.fullname || "",
}),
);
if (isRememberPassword) {
store.dispatch(
setDataLoginStorage({
usernameStorage: dataLoginStorage?.usernameStorage || "",
passwordStorage: form.password,
}),
);
} else {
store.dispatch(setDataLoginStorage(null));
}
router.replace(PATH.HOME);
},
});
const handleSubmit = () => {
loginMutation.mutate();
};
return (
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
<CustomLoading loading={loginMutation.isPending} />
{/* PASSWORD */}
<InputForm
label={
<span>
Mật khẩu mới
<span className="text-red-500"> *</span>
</span>
}
placeholder="Nhập mật khẩu mới"
type="password"
name="password"
onClean
isRequired
isBlur
showDone
icon={<ShieldCheck size={22} />}
/>
{/* RE PASSWORD */}
<div className="mt-5">
<InputForm
label={
<span>
Xác nhận mật khẩu mới
<span className="text-red-500"> *</span>
</span>
}
placeholder="Xác nhận mật khẩu mới"
type="password"
name="rePassword"
valueConfirm={form.password}
onClean
isRequired
isBlur
showDone
icon={<ShieldCheck size={22} />}
/>
</div>
{/* BUTTON */}
<div className="mt-6">
<ContextFormCustom.Consumer>
{({ isDone }) => (
<CustomButton
variant="midnightBlue"
rounded="full"
disabled={!isDone}
className="py-[10px] font-bold"
>
Lấy lại mật khẩu
</CustomButton>
)}
</ContextFormCustom.Consumer>
</div>
</FormCustom>
);
}
@@ -0,0 +1,16 @@
import { createContext, Dispatch, SetStateAction } from "react";
import { IFormForgotPassword, TYPE_FORGOT_PASWORD } from "../interface";
export interface IContextForgotPassword {
form: IFormForgotPassword;
setForm: Dispatch<SetStateAction<IFormForgotPassword>>;
type: number;
setType: Dispatch<SetStateAction<number>>;
}
export const ContextForgotPassword = createContext<IContextForgotPassword>({
form: { email: "", otp: "", password: "", rePassword: "" },
setForm: () => null,
type: TYPE_FORGOT_PASWORD.EMAIL,
setType: () => null,
});
@@ -0,0 +1,11 @@
export interface IFormForgotPassword {
email: string;
otp: string;
password: string;
rePassword: string;
}
export enum TYPE_FORGOT_PASWORD {
EMAIL = 1,
PASSWORD,
}
@@ -0,0 +1,269 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { LockKeyhole, ShieldPlus, User } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState, store } from "@/redux/store";
import {
setDataLoginStorage,
setStateLogin,
setToken,
} from "@/redux/reducer/auth";
import { setRememberPassword } from "@/redux/reducer/site";
import { setInfoUser } from "@/redux/reducer/user";
import authServices from "@/services/authServices";
import { httpRequest } from "@/services";
import { PATH } from "@/constant/config";
import FormCustom from "@/components/utils/FormCustom";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import CustomLoading from "@/components/customs/custom-loading";
// import FormCustom, { InputForm } from "@/components/common/FormCustom";
export default function MainLogin() {
const router = useRouter();
const { dataLoginStorage } = useSelector((state: RootState) => state.auth);
const { isRememberPassword } = useSelector((state: RootState) => state.site);
const [form, setForm] = useState({
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
});
const loginMutation = useMutation({
mutationFn: async () => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Đăng nhập thành công!",
http: authServices.login({
username: form.username,
password: form.password,
ip: "",
address: "",
type: 0,
}),
});
},
onSuccess(data: any) {
if (!data) return;
store.dispatch(setStateLogin(true));
store.dispatch(setToken(data.accessToken));
store.dispatch(
setInfoUser({
accessToken: data?.accessToken || "",
refreshToken: data?.refreshToken || "",
accessExpiresAt: data?.accessExpiresAt || "",
refreshExpiresAt: data?.refreshExpiresAt || "",
avatar: data?.avatar || "",
fullname: data?.fullname || "",
}),
);
if (isRememberPassword) {
store.dispatch(
setDataLoginStorage({
usernameStorage: form.username,
passwordStorage: form.password,
}),
);
} else {
store.dispatch(setDataLoginStorage(null));
}
router.replace(PATH.HOME);
},
});
const handleLogin = () => {
loginMutation.mutate();
};
return (
<div className="w-full">
<CustomLoading loading={loginMutation.isPending} />
<FormCustom form={form} setForm={setForm} onSubmit={handleLogin}>
{/* TITLE */}
<h3
className="
text-[34px]
font-semibold
text-[#1A1B2D]
"
>
Đăng nhập
</h3>
<p
className="
mt-1
text-[14px]
font-medium
text-[#6F767E]
"
>
Chào mừng bạn đến với hệ thống quản
</p>
{/* FORM */}
<div className="mt-6">
{/* USERNAME */}
<InputForm
label={
<span>
Tài khoản
<span className="text-red-500"> *</span>
</span>
}
placeholder="Tài khoản"
type="text"
name="username"
onClean
isRequired
isBlur
showDone
icon={<User size={22} />}
/>
{/* PASSWORD */}
<div className="mt-5">
<InputForm
label={
<span>
Mật khẩu
<span className="text-red-500"> *</span>
</span>
}
placeholder="Mật khẩu"
type="password"
name="password"
onClean
isRequired
isBlur
showDone
icon={<ShieldPlus size={22} />}
/>
</div>
{/* REMEMBER PASSWORD */}
<div
className="
mt-[14px]
flex
items-center
gap-2
cursor-pointer
"
>
<input
id="rememberPassword"
type="checkbox"
checked={isRememberPassword}
onChange={() =>
store.dispatch(setRememberPassword(!isRememberPassword))
}
className="
h-5
w-5
cursor-pointer
accent-[#0011AB]
"
/>
<label
htmlFor="rememberPassword"
className="
cursor-pointer
select-none
text-[14px]
font-medium
text-[#171717]
"
>
Nhớ mật khẩu
</label>
</div>
{/* BUTTONS */}
<div className="mt-6">
{/* LOGIN */}
<button
type="submit"
disabled={loginMutation.isPending}
className="
flex
h-[52px]
w-full
items-center
justify-center
rounded-full
bg-[#0011AB]
px-6
text-white
font-bold
transition-all
hover:opacity-90
disabled:cursor-not-allowed
disabled:opacity-50
"
>
Đăng nhập
</button>
{/* LINE */}
<div
className="
my-5
h-[1px]
w-full
bg-[#E5E5E5]
"
/>
{/* FORGOT PASSWORD */}
<button
type="button"
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
className="
flex
h-[52px]
w-full
items-center
justify-center
gap-2
rounded-full
border
border-gray-300
bg-white
px-6
font-bold
text-[#171717]
transition-all
hover:bg-gray-50
"
>
<LockKeyhole size={22} />
Quên mật khẩu
</button>
</div>
</div>
</FormCustom>
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./MainLogin";