update redux

This commit is contained in:
Mac mini
2026-03-31 17:32:16 +07:00
parent 4f92253534
commit 1674632b0f
9 changed files with 226 additions and 24 deletions

127
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19",
"@react-jvectormap/core": "^1.0.4", "@react-jvectormap/core": "^1.0.4",
"@react-jvectormap/world": "^1.1.2", "@react-jvectormap/world": "^1.1.2",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
@@ -29,6 +30,7 @@
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0"
@@ -1607,9 +1609,10 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.4", "version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -2654,12 +2657,50 @@
"resolved": "https://registry.npmjs.org/@react-jvectormap/world/-/world-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@react-jvectormap/world/-/world-1.1.2.tgz",
"integrity": "sha512-jDSy64DZz4mzEOrVni1zV5rak65+1WZhSGsvAvzHNIMHVx+tdD8R5OikyWUjYte8nxIkOUhQZtVJMK7VOIsmQA==" "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": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true "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": { "node_modules/@svgdotjs/svg.draggable.js": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
@@ -3355,6 +3396,12 @@
"@types/react": "*" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.49.0", "version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
@@ -4766,6 +4813,15 @@
"redux": "^4.2.0" "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": { "node_modules/doctrine": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -5971,6 +6027,16 @@
"node": ">= 4" "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": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "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", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"node_modules/redux": { "node_modules/react-redux": {
"version": "4.2.1", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": { "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": { "node_modules/reflect.getprototypeof": {
@@ -7682,6 +7778,12 @@
"regjsparser": "bin/parser" "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": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -8666,6 +8768,15 @@
"punycode": "^2.1.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -17,6 +17,7 @@
"@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19",
"@react-jvectormap/core": "^1.0.4", "@react-jvectormap/core": "^1.0.4",
"@react-jvectormap/world": "^1.1.2", "@react-jvectormap/world": "^1.1.2",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",
@@ -30,6 +31,7 @@
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0"

View File

@@ -1,16 +1,14 @@
"use client"
import UserAddressCard from "@/components/user-profile/UserAddressCard"; import UserAddressCard from "@/components/user-profile/UserAddressCard";
import UserInfoCard from "@/components/user-profile/UserInfoCard"; import UserInfoCard from "@/components/user-profile/UserInfoCard";
import UserMetaCard from "@/components/user-profile/UserMetaCard"; import UserMetaCard from "@/components/user-profile/UserMetaCard";
import { Metadata } from "next"; import { RootState } from "@/store/store";
import React from "react"; import { useSelector } from "react-redux";
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",
};
export default function Profile() { export default function Profile() {
const user = useSelector((state: RootState) => state.user.data);
console.log("Current User:", user);
return ( return (
<div> <div>
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6"> <div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">

View File

@@ -4,6 +4,7 @@ import "flatpickr/dist/flatpickr.css";
import { SidebarProvider } from '@/context/SidebarContext'; import { SidebarProvider } from '@/context/SidebarContext';
import { ThemeProvider } from '@/context/ThemeContext'; import { ThemeProvider } from '@/context/ThemeContext';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import StoreProvider from '@/store/StoreProvider';
const outfit = Outfit({ const outfit = Outfit({
subsets: ["latin"], subsets: ["latin"],
@@ -17,9 +18,11 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body className={`${outfit.className} dark:bg-gray-900`}> <body className={`${outfit.className} dark:bg-gray-900`}>
<ThemeProvider> <StoreProvider>
<SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider> <ThemeProvider>
</ThemeProvider> <SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider>
</ThemeProvider>
</StoreProvider>
</body> </body>
</html> </html>
); );

View File

@@ -3,17 +3,20 @@
import Checkbox from "@/components/form/input/Checkbox"; import Checkbox from "@/components/form/input/Checkbox";
import Input from "@/components/form/input/InputField"; import Input from "@/components/form/input/InputField";
import Label from "@/components/form/Label"; import Label from "@/components/form/Label";
import Button from "@/components/ui/button/Button";
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons"; import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
import { apiGetCurrentUser, apiSignIn } from "@/service/auth"; import { apiGetCurrentUser, apiSignIn } from "@/service/auth";
import Link from "next/link"; import Link from "next/link";
import React, { useState } from "react"; import React, { useState } from "react";
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from "../../../api"; 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() { 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 [isChecked, setIsChecked] = useState(false);
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -60,11 +63,15 @@ const [showPassword, setShowPassword] = useState(false);
setLoading(true); setLoading(true);
const res = await apiSignIn(formData); const res = await apiSignIn(formData);
console.log("API Sign In Response:", res); console.log("API Sign In Response:", res);
if (res.status === true) { if (res.status === true) {
toast.success('Đăng nhập thành công!'); toast.success('Đăng nhập thành công!');
const data = await apiGetCurrentUser(); 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{ }else{
toast.error('Email hoặc mật khẩu không đúng.'); toast.error('Email hoặc mật khẩu không đúng.');
} }

27
src/interface/user.ts Normal file
View File

@@ -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[];
}

View File

@@ -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 <Provider store={storeRef.current}>{children}</Provider>;
}

View File

@@ -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<UserData>) => {
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;

11
src/store/store.ts Normal file
View File

@@ -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<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;