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,35 @@
"use client";
import { useEffect } from "react";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
function LoadingTopBar() {
useEffect(() => {
NProgress.configure({ showSpinner: false });
const handleClick = (e: any) => {
const target = e.target.closest("a");
if (target?.href && target.href.startsWith(window.location.origin)) {
NProgress.start();
}
};
const handleStop = () => {
NProgress.done();
};
window.addEventListener("click", handleClick);
window.addEventListener("load", handleStop);
return () => {
window.removeEventListener("click", handleClick);
window.removeEventListener("load", handleStop);
};
}, []);
return null;
}
export default LoadingTopBar;
@@ -0,0 +1 @@
export { default } from "./LoadingTopBar";
@@ -0,0 +1,30 @@
"use client";
import { PATH } from "@/constant/config";
import { RootState } from "@/redux/store";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useSelector } from "react-redux";
interface IRequireAuthProps {
children: React.ReactNode;
}
export default function RequireAuth(props: IRequireAuthProps) {
const router = useRouter();
const { loading } = useSelector((state: RootState) => state.site);
const { isLogin } = useSelector((state: RootState) => state.auth);
useEffect(() => {
if (!isLogin && !loading) {
router.replace(PATH.LOGIN);
}
}, [isLogin, loading, router]);
if (isLogin && !loading) {
return <>{props.children}</>;
}
return <div className="loading-page" />;
}
@@ -0,0 +1,34 @@
//**********************
//* COMPONENT PROTECTED SCREEN THEN LOGIN
//**********************
"use client";
import React, { useEffect } from "react";
import { RootState } from "@/redux/store";
import { useRouter } from "next/navigation";
import { useSelector } from "react-redux";
import { PATH } from "@/constant/config";
interface props {
children: React.ReactNode;
}
function RequiredLogout({ children }: props) {
const { replace } = useRouter();
const { isLogin } = useSelector((state: RootState) => state.auth);
const { loading } = useSelector((state: RootState) => state.site);
useEffect(() => {
if (isLogin && !loading) replace(PATH.HOME);
}, [isLogin, loading, replace]);
if (!isLogin && !loading) {
return <>{children}</>;
}
return <div className="loading-page"></div>;
}
export default RequiredLogout;
@@ -0,0 +1,111 @@
"use client";
import { useEffect, useState } from "react";
import { RootState, store } from "@/redux/store";
import { useSelector } from "react-redux";
import clsx from "clsx";
import Lottie from "lottie-react";
import { getItemStorage, setItemStorage } from "@/common/funcs/localStorage";
import { KEY_STORE } from "@/constant/config";
import { setLoading, setRememberPassword } from "@/redux/reducer/site";
import {
setDataLoginStorage,
setStateLogin,
setToken,
} from "@/redux/reducer/auth";
import { setInfoUser } from "@/redux/reducer/user";
import loadingAnim from "@/../public/static/anim/loading_screen.json";
function SplashScreen() {
const [isClosing, setIsClosing] = useState(false);
const { loading, isRememberPassword } = useSelector(
(state: RootState) => state.site,
);
const { infoUser } = useSelector((state: RootState) => state.user);
const { token, isLogin, dataLoginStorage } = useSelector(
(state: RootState) => state.auth,
);
// 🔥 Load state từ localStorage
useEffect(() => {
(async () => {
const encryptedState = await getItemStorage(KEY_STORE);
const SECRET_KEY = process.env.NEXT_PUBLIC_SECRET_KEY || "default_key";
const decode = (data: string): any => {
try {
const decoded = decodeURIComponent(atob(data));
const clean = decoded.startsWith(SECRET_KEY)
? decoded.slice(SECRET_KEY.length)
: null;
return clean ? JSON.parse(clean) : null;
} catch (err) {
console.error("Decode error:", err);
return null;
}
};
const state =
typeof encryptedState === "string" ? decode(encryptedState) : null;
if (state) {
store.dispatch(setToken(state.token));
store.dispatch(setStateLogin(state.isLogin));
store.dispatch(setInfoUser(state.infoUser));
store.dispatch(setRememberPassword(state.isRememberPassword));
store.dispatch(setDataLoginStorage(state.dataLoginStorage));
}
store.dispatch(setLoading(false));
})();
}, []);
// 🔥 Save lại state
useEffect(() => {
if (!loading) {
const SECRET_KEY = process.env.NEXT_PUBLIC_SECRET_KEY || "default_key";
const encode = (data: string): string => {
const textToEncode = SECRET_KEY + data;
return btoa(encodeURIComponent(textToEncode));
};
const hashedData = encode(
JSON.stringify({
isLogin,
token,
infoUser,
isRememberPassword,
dataLoginStorage,
}),
);
setItemStorage(KEY_STORE, hashedData);
// 👉 trigger animation fade out
setTimeout(() => setIsClosing(true), 0);
}
}, [loading, isLogin, token, infoUser, isRememberPassword, dataLoginStorage]);
return (
<div
className={clsx(
"fixed inset-0 flex items-center justify-center z-[1000000] bg-white transition-opacity duration-300",
{
"opacity-0 invisible": !loading && isClosing,
"opacity-100 visible": loading,
},
)}
>
<div className="w-[40vw] max-w-[320px] max-h-[320px]">
<Lottie animationData={loadingAnim} loop autoplay />
</div>
</div>
);
}
export default SplashScreen;
@@ -0,0 +1 @@
export { default } from "./SplashScreen";