feat: implement authentication modules and add daily statistics dashboard component
Build and Release / release (push) Successful in 28s
Build and Release / release (push) Successful in 28s
This commit is contained in:
@@ -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 */}
|
||||
|
||||
-4
@@ -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({
|
||||
+1
-1
@@ -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 />
|
||||
@@ -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't have an account? {""}
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/auth/signup"
|
||||
className="text-brand-500 hover:text-brand-600 dark:text-brand-400"
|
||||
>
|
||||
Sign Up
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user