diff --git a/package-lock.json b/package-lock.json index 7dd621b..9b0c95e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@fullcalendar/timegrid": "^6.1.19", "@react-jvectormap/core": "^1.0.4", "@react-jvectormap/world": "^1.1.2", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.17", "apexcharts": "^4.7.0", @@ -29,6 +30,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", "react-dropzone": "^14.3.8", + "react-redux": "^9.2.0", "sonner": "^2.0.7", "swiper": "^11.2.10", "tailwind-merge": "^2.6.0" @@ -1607,9 +1609,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -2654,12 +2657,50 @@ "resolved": "https://registry.npmjs.org/@react-jvectormap/world/-/world-1.1.2.tgz", "integrity": "sha512-jDSy64DZz4mzEOrVni1zV5rak65+1WZhSGsvAvzHNIMHVx+tdD8R5OikyWUjYte8nxIkOUhQZtVJMK7VOIsmQA==" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@svgdotjs/svg.draggable.js": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", @@ -3355,6 +3396,12 @@ "@types/react": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.49.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", @@ -4766,6 +4813,15 @@ "redux": "^4.2.0" } }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5971,6 +6027,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7579,12 +7645,42 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.9.2" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" } }, "node_modules/reflect.getprototypeof": { @@ -7682,6 +7778,12 @@ "regjsparser": "bin/parser" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -8666,6 +8768,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b58ac82..c83abfc 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@fullcalendar/timegrid": "^6.1.19", "@react-jvectormap/core": "^1.0.4", "@react-jvectormap/world": "^1.1.2", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.17", "apexcharts": "^4.7.0", @@ -30,6 +31,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", "react-dropzone": "^14.3.8", + "react-redux": "^9.2.0", "sonner": "^2.0.7", "swiper": "^11.2.10", "tailwind-merge": "^2.6.0" diff --git a/src/app/(admin)/(others-pages)/profile/page.tsx b/src/app/(admin)/(others-pages)/profile/page.tsx index 8f14e27..ef43221 100644 --- a/src/app/(admin)/(others-pages)/profile/page.tsx +++ b/src/app/(admin)/(others-pages)/profile/page.tsx @@ -1,16 +1,14 @@ +"use client" + import UserAddressCard from "@/components/user-profile/UserAddressCard"; import UserInfoCard from "@/components/user-profile/UserInfoCard"; import UserMetaCard from "@/components/user-profile/UserMetaCard"; -import { Metadata } from "next"; -import React from "react"; - -export const metadata: Metadata = { - title: "Next.js Profile | TailAdmin - Next.js Dashboard Template", - description: - "This is Next.js Profile page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template", -}; +import { RootState } from "@/store/store"; +import { useSelector } from "react-redux"; export default function Profile() { + const user = useSelector((state: RootState) => state.user.data); + console.log("Current User:", user); return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 95cebf5..d4f2ed5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "flatpickr/dist/flatpickr.css"; import { SidebarProvider } from '@/context/SidebarContext'; import { ThemeProvider } from '@/context/ThemeContext'; import { Toaster } from 'sonner'; +import StoreProvider from '@/store/StoreProvider'; const outfit = Outfit({ subsets: ["latin"], @@ -17,9 +18,11 @@ export default function RootLayout({ return ( - - {children} - + + + {children} + + ); diff --git a/src/components/auth/SignInForm.tsx b/src/components/auth/SignInForm.tsx index 5b3022b..d6b8819 100644 --- a/src/components/auth/SignInForm.tsx +++ b/src/components/auth/SignInForm.tsx @@ -3,17 +3,20 @@ import Checkbox from "@/components/form/input/Checkbox"; import Input from "@/components/form/input/InputField"; import Label from "@/components/form/Label"; -import Button from "@/components/ui/button/Button"; import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons"; import { apiGetCurrentUser, apiSignIn } from "@/service/auth"; import Link from "next/link"; import React, { useState } from "react"; import { toast } from 'sonner'; import { API } from "../../../api"; -import api from "@/config/config"; +import { setUserData } from "@/store/features/userSlice"; +import { useDispatch } from "react-redux"; +import { useRouter } from "next/navigation"; export default function SignInForm() { -const [showPassword, setShowPassword] = useState(false); + const router = useRouter(); + const [showPassword, setShowPassword] = useState(false); + const dispatch = useDispatch(); const [isChecked, setIsChecked] = useState(false); const [errorMsg, setErrorMsg] = useState(""); const [loading, setLoading] = useState(false); @@ -60,11 +63,15 @@ const [showPassword, setShowPassword] = useState(false); setLoading(true); const res = await apiSignIn(formData); console.log("API Sign In Response:", res); + if (res.status === true) { toast.success('Đăng nhập thành công!'); - const data = await apiGetCurrentUser(); - console.log("Current User:", data); + console.log("Current User Data:", data); + if (data?.data) { + dispatch(setUserData(data.data)); + // router.push("/profile"); + } }else{ toast.error('Email hoặc mật khẩu không đúng.'); } diff --git a/src/interface/user.ts b/src/interface/user.ts new file mode 100644 index 0000000..05f4cce --- /dev/null +++ b/src/interface/user.ts @@ -0,0 +1,27 @@ +export interface UserProfile { + display_name: string; + full_name: string; + avatar_url: string; + bio: string; + location: string; + website: string; + country_code: string; + phone: string; +} + +export interface UserRole { + id: string; + name: string; +} + +export interface UserData { + id: string; + email: string; + profile: UserProfile; + token_version: number; + is_deleted: boolean; + created_at: string; + updated_at: string; + roles: UserRole[]; +} + diff --git a/src/store/StoreProvider.tsx b/src/store/StoreProvider.tsx new file mode 100644 index 0000000..f4adf37 --- /dev/null +++ b/src/store/StoreProvider.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { useRef } from 'react'; +import { Provider } from 'react-redux'; +import { store } from './store'; + +export default function StoreProvider({ + children, +}: { + children: React.ReactNode; +}) { + const storeRef = useRef(store); + return {children}; +} \ No newline at end of file diff --git a/src/store/features/userSlice.ts b/src/store/features/userSlice.ts new file mode 100644 index 0000000..df0381a --- /dev/null +++ b/src/store/features/userSlice.ts @@ -0,0 +1,29 @@ +import { UserData } from '@/interface/user'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface UserState { + data: UserData | null; + isAuthenticated: boolean; +} +const initialState: UserState = { + data: null, + isAuthenticated: false, +}; + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + setUserData: (state, action: PayloadAction) => { + state.data = action.payload; + state.isAuthenticated = true; + }, + clearUserData: (state) => { + state.data = null; + state.isAuthenticated = false; + }, + }, +}); + +export const { setUserData, clearUserData } = userSlice.actions; +export default userSlice.reducer; \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..c3c8be0 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from '@reduxjs/toolkit'; +import userReducer from './features/userSlice'; + +export const store = configureStore({ + reducer: { + user: userReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file