feat: implement authentication modules and add daily statistics dashboard component
Build and Release / release (push) Successful in 28s

This commit is contained in:
2026-05-14 11:40:37 +07:00
parent 6363377b9a
commit a7c3aecc6e
9 changed files with 70 additions and 26 deletions
+60 -9
View File
@@ -1,13 +1,17 @@
"use client";
import { useSidebar } from "@/context/SidebarContext";
import { UserData } from "@/interface/user";
import AppHeader from "@/layout/AppHeader";
import AppSidebar from "@/layout/AppSidebar";
import Backdrop from "@/layout/Backdrop";
import { apiGetCurrentUser } from "@/service/auth";
import { setUserData } from "@/store/features/userSlice";
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { RootState } from "@/store/store";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "sonner";
export default function AdminLayout({
children,
@@ -15,20 +19,44 @@ export default function AdminLayout({
children: React.ReactNode;
}) {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
const dispatch = useDispatch()
const dispatch = useDispatch();
const router = useRouter();
const userData = useSelector((state: RootState) => state.user.data);
const [isLoading, setIsLoading] = useState(!userData);
useEffect(() => {
if (userData) {
setIsLoading(false);
return;
}
const fetchUser = async () => {
try {
const userData = await apiGetCurrentUser();
dispatch(setUserData(userData.data));
} catch (err) {
console.error("Lỗi:", err);
setIsLoading(true);
const res = await apiGetCurrentUser();
const userData: UserData = res;
const allowedRoles = ["ADMIN", "MOD", "HISTORIAN"];
const isBanned = userData.roles.some((role) => role.name === "BANNED");
const hasPermission = userData.roles.some((role) =>
allowedRoles.includes(role.name)
);
if (isBanned || !hasPermission) {
toast.error("Bạn không có quyền truy cập");
router.replace("/auth/signin");
}
dispatch(setUserData(res.data));
} catch {
router.replace("/auth/signin");
} finally {
setIsLoading(false);
}
};
fetchUser();
}, [])
}, [dispatch, userData, router]);
// Dynamic class for main content margin based on sidebar state
const mainContentMargin = isMobileOpen
@@ -37,6 +65,29 @@ export default function AdminLayout({
? "lg:ml-[290px]"
: "lg:ml-[90px]";
if (isLoading || !userData) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900">
<div className="flex flex-col items-center gap-5">
{/* Spinner */}
<div className="relative h-14 w-14">
<div className="absolute inset-0 rounded-full border-4 border-gray-200 dark:border-gray-700" />
<div className="absolute inset-0 rounded-full border-4 border-transparent border-t-blue-500 animate-spin" />
</div>
{/* Text */}
<div className="flex flex-col items-center gap-1.5">
<p className="text-base font-semibold text-gray-800 dark:text-gray-100">
Đang tải dữ liệu
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 animate-pulse">
Vui lòng chờ trong giây lát...
</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen xl:flex">
{/* Sidebar and Backdrop */}
@@ -1,9 +1,5 @@
import GridShape from "@/components/common/GridShape";
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
import { ThemeProvider } from "@/context/ThemeContext";
import Image from "next/image";
import Link from "next/link";
import React from "react";
export default function AuthLayout({
@@ -100,7 +100,7 @@ export default function ResetPasswordForm() {
<div className="flex flex-col flex-1 w-full lg:w-1/2 overflow-y-auto no-scrollbar">
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
<Link
href="/signin"
href="/auth/signin"
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<ChevronLeftIcon />
+3 -4
View File
@@ -71,7 +71,6 @@ export default function SignInForm() {
toast.success("Đăng nhập thành công!");
const data = await apiGetCurrentUser();
// console.log("Current User Data:", data);
if (data?.data) {
dispatch(setUserData(data.data));
router.push("/");
@@ -79,7 +78,7 @@ export default function SignInForm() {
} else {
toast.error("Email hoặc mật khẩu không đúng.");
}
} catch (error) {
} catch {
setErrorMsg("Lỗi khi đăng nhập. Vui lòng thử lại.");
toast.error("Đăng nhập thất bại. Vui lòng kiểm tra lại thông tin.");
} finally {
@@ -206,7 +205,7 @@ export default function SignInForm() {
</span>
</div>
<Link
href="/reset-password"
href="/auth/reset-password"
className="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
Forgot password?
@@ -256,7 +255,7 @@ export default function SignInForm() {
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
Don&apos;t have an account? {""}
<Link
href="/signup"
href="/auth/signup"
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
Sign Up
+4 -4
View File
@@ -72,7 +72,7 @@ export default function SignUpForm() {
setLoading(true);
await apiCreateOTP(formData.email);
setStep(2);
} catch (error) {
} catch {
setErrorMsg("Lỗi khi tạo OTP. Vui lòng thử lại.");
toast.error("Tạo OTP thất bại. Vui lòng kiểm tra lại thông tin.");
} finally {
@@ -106,7 +106,7 @@ export default function SignUpForm() {
token_id: tokenId,
};
const signupRes = await apiSignUp(signupPayload);
await apiSignUp(signupPayload);
await Swal.fire({
title: "Tạo tài khoản thành công!. Quay về trang đăng nhập để tiếp tục",
@@ -131,7 +131,7 @@ export default function SignUpForm() {
<div className="flex flex-col flex-1 lg:w-1/2 w-full overflow-y-auto no-scrollbar">
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
<Link
href="/signin"
href="/auth/signin"
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<ChevronLeftIcon />
@@ -332,7 +332,7 @@ export default function SignUpForm() {
<p className="text-sm font-normal text-center text-gray-700 dark:text-gray-400 sm:text-start">
Already have an account?{" "}
<Link
href="/signin"
href="/auth/signin"
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
Sign In
+1 -3
View File
@@ -31,7 +31,6 @@ export default function MonthlyNewChart() {
try {
const response = await getStatistics({ start_date: start, end_date: end });
if (response?.data) {
// Tổng hợp theo tháng
const monthsMap: { [key: string]: number } = {};
response.data.forEach(item => {
@@ -56,8 +55,7 @@ export default function MonthlyNewChart() {
setChartData(aggregated);
}
} catch (error) {
console.error("Failed to fetch chart data", error);
} catch {
} finally {
setLoading(false);
}
+1 -1
View File
@@ -67,7 +67,7 @@ api.interceptors.response.use(
isRefreshing = true;
try {
await axios.post(`${baseURL}/auth/refresh`, {}, { withCredentials: true });
await axios.post(`${baseURL}/auth/refresh`, undefined, { withCredentials: true });
processQueue(null);