Compare commits
6 Commits
7645838eb8
...
feat/flow-
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a32b0fecc | |||
| 479b57cd1e | |||
| fe093ab8dc | |||
| b74ef6a892 | |||
| c17f0d3d9c | |||
| 21e77456c9 |
8
.env
8
.env
@@ -1 +1,7 @@
|
|||||||
VITE_SV_HOST="http://localhost:3000"
|
VITE_SV_HOST="http://localhost:3000"
|
||||||
|
VITE_JWT_TOKEN="phuocntbasdasasdasd"
|
||||||
|
|
||||||
|
VITE_CLOUDINARY_UPLOAD_PRESET="testpreset"
|
||||||
|
VITE_CLOUDINARY_CLOUD_NAME="dusw32tsq"
|
||||||
|
VITE_CLOUDINARY_API_SECRET="NeUuUfbjlZSSyq0Zf390LECRdcI"
|
||||||
|
VITE_CLOUDINARY_API_KEY="114544946391646"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
(Video)[https://rikkeieducation.sg.larksuite.com/minutes/obsgcl8da4w9x316w6jae174]
|
||||||
## Rikkei Store
|
## Rikkei Store
|
||||||
## Bán máy tính & điện thoại
|
## Bán máy tính & điện thoại
|
||||||
|
|
||||||
@@ -14,3 +15,9 @@
|
|||||||
- Đăng ký / đăng nhập X
|
- Đăng ký / đăng nhập X
|
||||||
- Layout người dùng X
|
- Layout người dùng X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Cloudinary
|
||||||
|
npm i cloudinary
|
||||||
|
|
||||||
|
## Update test
|
||||||
101
package-lock.json
generated
101
package-lock.json
generated
@@ -8,11 +8,15 @@
|
|||||||
"name": "react_project",
|
"name": "react_project",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@reduxjs/toolkit": "^2.9.0",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@tinymce/tinymce-react": "^6.3.0",
|
||||||
"antd": "^5.27.4",
|
"antd": "^5.27.4",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"cloudinary": "^2.7.0",
|
||||||
|
"jose": "^6.1.0",
|
||||||
"json-server": "^1.0.0-beta.3",
|
"json-server": "^1.0.0-beta.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -2461,6 +2465,25 @@
|
|||||||
"node": ">=12.20"
|
"node": ">=12.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tinymce/tinymce-react": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||||
|
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
|
||||||
|
"tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"tinymce": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -3176,6 +3199,19 @@
|
|||||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cloudinary": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-qrqDn31+qkMCzKu1GfRpzPNAO86jchcNwEHCUiqvPHNSFqu7FTNF9FuAkBUyvM1CFFgFPu64NT0DyeREwLwK0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"q": "^1.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -4157,11 +4193,19 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -4539,6 +4583,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -4546,6 +4596,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lowdb": {
|
"node_modules/lowdb": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
|
||||||
@@ -4756,6 +4818,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -4896,6 +4967,23 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -4912,6 +5000,17 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/q": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
|
||||||
|
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0",
|
||||||
|
"teleport": ">=0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|||||||
@@ -11,11 +11,15 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@reduxjs/toolkit": "^2.9.0",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@tinymce/tinymce-react": "^6.3.0",
|
||||||
"antd": "^5.27.4",
|
"antd": "^5.27.4",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"cloudinary": "^2.7.0",
|
||||||
|
"jose": "^6.1.0",
|
||||||
"json-server": "^1.0.0-beta.3",
|
"json-server": "^1.0.0-beta.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|||||||
13
src/App.tsx
13
src/App.tsx
@@ -1,9 +1,18 @@
|
|||||||
import { createContext } from 'react'
|
import { createContext } from 'react'
|
||||||
import RouterSetup from './RouterSetup'
|
import RouterSetup from './RouterSetup'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import type { StoreType } from './stores'
|
||||||
|
import Loading from './components/Loading'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const userStore = useSelector((store: StoreType) => store.user)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouterSetup />
|
<>
|
||||||
|
{
|
||||||
|
userStore.loading ? <Loading />
|
||||||
|
: <RouterSetup />
|
||||||
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ import Admin from './pages/admin/Admin'
|
|||||||
import ProtectedAdmin from './pages/admin/auth/ProtectedAdmin'
|
import ProtectedAdmin from './pages/admin/auth/ProtectedAdmin'
|
||||||
import Auth from './pages/home/auth/Auth'
|
import Auth from './pages/home/auth/Auth'
|
||||||
import UserManagement from './pages/admin/User/UserManagement'
|
import UserManagement from './pages/admin/User/UserManagement'
|
||||||
|
import CategoryManagement from './pages/admin/Category/CategoryManagement'
|
||||||
|
import ProductManagement from './pages/admin/Product/ProductManagement'
|
||||||
|
import CreateProductForm from './pages/admin/Product/components/CreateProductForm'
|
||||||
|
|
||||||
export default function RouterSetup() {
|
export default function RouterSetup() {
|
||||||
return <Routes>
|
return <Routes>
|
||||||
<Route path='/' element={<Home/>}></Route>
|
<Route path='/' element={<Home />}></Route>
|
||||||
<Route path='/auth' element={<Auth/>}></Route>
|
<Route path='/auth' element={<Auth />}></Route>
|
||||||
<Route path='admin' element={
|
<Route path='admin' element={
|
||||||
<ProtectedAdmin>
|
<ProtectedAdmin>
|
||||||
<Admin/>
|
<Admin />
|
||||||
</ProtectedAdmin>
|
</ProtectedAdmin>
|
||||||
}>
|
}>
|
||||||
<Route path='user' element={<UserManagement/>}></Route>
|
<Route path='user' element={<UserManagement />}></Route>
|
||||||
|
<Route path='category' element={<CategoryManagement />}></Route>
|
||||||
|
<Route path='product' element={<ProductManagement />}>
|
||||||
|
<Route path='add' element={<CreateProductForm />}></Route>
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/apis/core/category.api.ts
Normal file
20
src/apis/core/category.api.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
|
||||||
|
export const CategoryApi = {
|
||||||
|
getAll: async () => {
|
||||||
|
let resultRes = await axios.get(`${import.meta.env.VITE_SV_HOST}/categories`)
|
||||||
|
return resultRes.data
|
||||||
|
},
|
||||||
|
create: async (data: {
|
||||||
|
title: string;
|
||||||
|
iconUrl: string
|
||||||
|
}) => {
|
||||||
|
let result = await axios.post(`${import.meta.env.VITE_SV_HOST}/categories`, {
|
||||||
|
...data,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/apis/core/cloudinary.api.ts
Normal file
12
src/apis/core/cloudinary.api.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export const CloudianryApi = {
|
||||||
|
upload: async (file: File) => {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
formData.append("upload_preset", import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET)
|
||||||
|
formData.append("cloud_name", import.meta.env.VITE_CLOUDINARY_CLOUD_NAME)
|
||||||
|
let result = await axios.post(`https://api.cloudinary.com/v1_1/${import.meta.env.VITE_CLOUDINARY_CLOUD_NAME}/image/upload`, formData)
|
||||||
|
return result.data.secure_url
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/apis/core/product.api.ts
Normal file
23
src/apis/core/product.api.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export interface CreateProductDTO {
|
||||||
|
categorieId: string;
|
||||||
|
price: number;
|
||||||
|
iconUrl: string;
|
||||||
|
name: string;
|
||||||
|
des: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductApi = {
|
||||||
|
findAll: async () => {
|
||||||
|
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/products`)
|
||||||
|
return result.data
|
||||||
|
},
|
||||||
|
create: async (data: CreateProductDTO) => {
|
||||||
|
let result = await axios.post(`${import.meta.env.VITE_SV_HOST}/products`, {
|
||||||
|
...data,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import axios from "axios"
|
|||||||
import type { User } from "../../types/user.type"
|
import type { User } from "../../types/user.type"
|
||||||
import { message } from "antd"
|
import { message } from "antd"
|
||||||
import { ApiUtil } from "../../utils/api.util"
|
import { ApiUtil } from "../../utils/api.util"
|
||||||
|
import * as jose from 'jose'
|
||||||
|
|
||||||
export interface UserSignInDTO {
|
export interface UserSignInDTO {
|
||||||
emailOrUserName: string
|
emailOrUserName: string
|
||||||
@@ -16,10 +17,7 @@ export interface UserFindAllDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UserApi = {
|
export const UserApi = {
|
||||||
signIn: async (data: UserSignInDTO): Promise<{
|
signIn: async (data: UserSignInDTO) => {
|
||||||
message: string
|
|
||||||
data: any
|
|
||||||
}> => {
|
|
||||||
let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`)
|
let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`)
|
||||||
if (userData.data.length == 0) {
|
if (userData.data.length == 0) {
|
||||||
userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`)
|
userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`)
|
||||||
@@ -36,10 +34,7 @@ export const UserApi = {
|
|||||||
data: null
|
data: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return createToken(userData.data[0].id)
|
||||||
message: "Đăng nhập thành công!",
|
|
||||||
data: userData.data[0] as User
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
signUp: async (data: User) => {
|
signUp: async (data: User) => {
|
||||||
@@ -106,5 +101,59 @@ export const UserApi = {
|
|||||||
findAll: async (query?: UserFindAllDTO) => {
|
findAll: async (query?: UserFindAllDTO) => {
|
||||||
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query))
|
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query))
|
||||||
return result.data
|
return result.data
|
||||||
|
},
|
||||||
|
me: async (token: string) => {
|
||||||
|
let tokenData = await decodeToken(token)
|
||||||
|
|
||||||
|
if (!tokenData) {
|
||||||
|
throw ({
|
||||||
|
message: "Token không chính xác!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let { userId } = tokenData;
|
||||||
|
|
||||||
|
let getUserByIdRes = await axios.get(`${import.meta.env.VITE_SV_HOST}/users/${userId}`)
|
||||||
|
|
||||||
|
if (!getUserByIdRes.data) {
|
||||||
|
throw ({
|
||||||
|
message: "Lỗi lấy dữ liệu"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(getUserByIdRes.data)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function createToken(userId: string) {
|
||||||
|
const secret = new TextEncoder().encode(import.meta.env.VITE_JWT_TOKEN);
|
||||||
|
|
||||||
|
const token = await new jose.SignJWT({ userId })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('2h')
|
||||||
|
.sign(secret);
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeToken(token: string) {
|
||||||
|
try {
|
||||||
|
const secret = new TextEncoder().encode(import.meta.env.VITE_JWT_TOKEN);
|
||||||
|
|
||||||
|
const { payload } = await jose.jwtVerify(token, secret, {
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token không hợp lệ hoặc đã hết hạn:', error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { CategoryApi } from "./core/category.api";
|
||||||
|
import { CloudianryApi } from "./core/cloudinary.api";
|
||||||
|
import { ProductApi } from "./core/product.api";
|
||||||
import { UserApi } from "./core/user.api";
|
import { UserApi } from "./core/user.api";
|
||||||
|
|
||||||
export const Apis = {
|
export const Apis = {
|
||||||
user: UserApi
|
user: UserApi,
|
||||||
|
category: CategoryApi,
|
||||||
|
cloundInary: CloudianryApi,
|
||||||
|
product: ProductApi
|
||||||
}
|
}
|
||||||
22
src/components/Loading.tsx
Normal file
22
src/components/Loading.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-gray-200 animate-pulse" />
|
||||||
|
<svg
|
||||||
|
className="absolute -right-2 -bottom-2 h-7 w-7 animate-spin"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" className="text-gray-300 opacity-70" />
|
||||||
|
<path d="M22 12a10 10 0 00-10-10" stroke="currentColor" strokeWidth="3" className="text-indigo-500" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600 font-medium">Đang tải dữ liệu xác thực...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
163
src/pages/admin/Category/CategoryManagement.tsx
Normal file
163
src/pages/admin/Category/CategoryManagement.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useEffect, useState, type FormEvent } from 'react'
|
||||||
|
import { Apis } from '../../../apis'
|
||||||
|
import type { Category } from '../../../types/category.type'
|
||||||
|
import { Button, Form, Input, Modal, Space, Table, Upload } from 'antd'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default function CategoryManagement() {
|
||||||
|
/* State Modal */
|
||||||
|
const [createNewModalState, setCreateNewModalState] = useState(false)
|
||||||
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
|
|
||||||
|
const [loadingState, setLoadingState] = useState(false)
|
||||||
|
|
||||||
|
async function getCategories() {
|
||||||
|
try {
|
||||||
|
let result = await Apis.category.getAll()
|
||||||
|
setCategories(result)
|
||||||
|
} catch (err) {
|
||||||
|
/* err */
|
||||||
|
alert("tạch!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCategories()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Số Thứ Tự',
|
||||||
|
key: 'index',
|
||||||
|
render: (_: any, __: any, index: number) => index + 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tiêu Đề',
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Icon',
|
||||||
|
dataIndex: 'iconUrl',
|
||||||
|
key: 'iconUrl',
|
||||||
|
render: (_: any, record: Category) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<img src={record.iconUrl} style={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
borderRadius: "50"
|
||||||
|
}} />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Trạng Thái',
|
||||||
|
dataIndex: 'isActive',
|
||||||
|
key: 'isActive',
|
||||||
|
render: (_: any, record: Category) => (
|
||||||
|
<Space size="middle">
|
||||||
|
{
|
||||||
|
record.isActive ?
|
||||||
|
<Button color="danger" variant="solid">Ngừng Hoạt Động</Button>
|
||||||
|
: <Button type='primary'>Kích Hoạt</Button>
|
||||||
|
}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Công Cụ',
|
||||||
|
dataIndex: 'isActive',
|
||||||
|
key: 'isActive',
|
||||||
|
render: (_: any, record: Category) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Button color="danger" variant="solid">Xóa</Button>
|
||||||
|
<Button type='primary'>Sửa</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/* form */
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: "relative"
|
||||||
|
}}>
|
||||||
|
<h1>Quản Lý Danh Mục</h1>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setCreateNewModalState(true)
|
||||||
|
}} type='primary'>Thêm Mới</Button>
|
||||||
|
<Table
|
||||||
|
dataSource={categories}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
{/* Modal Create new */}
|
||||||
|
<Modal
|
||||||
|
title="Thêm mới danh mục"
|
||||||
|
closable={{ 'aria-label': 'Custom Close Button' }}
|
||||||
|
open={createNewModalState}
|
||||||
|
onOk={() => {
|
||||||
|
if(loadingState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// setCreateNewModalState(false)
|
||||||
|
try {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then(async (values) => {
|
||||||
|
setLoadingState(true)
|
||||||
|
let result = await Apis.cloundInary.upload(values.file.file)
|
||||||
|
let newCategory = {
|
||||||
|
title: values.title,
|
||||||
|
iconUrl: result
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultRes = await Apis.category.create(newCategory)
|
||||||
|
setCategories([
|
||||||
|
...categories,
|
||||||
|
resultRes
|
||||||
|
])
|
||||||
|
setLoadingState(false)
|
||||||
|
setCreateNewModalState(false)
|
||||||
|
})
|
||||||
|
} catch (er) {
|
||||||
|
setLoadingState(false)
|
||||||
|
setCreateNewModalState(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setCreateNewModalState(false)
|
||||||
|
}}
|
||||||
|
okButtonProps={{
|
||||||
|
htmlType: "submit"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="Tên danh mục"
|
||||||
|
rules={[{ required: true, message: 'Nhập tên danh mục!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Nhập title" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="file" label="Ảnh">
|
||||||
|
<Upload beforeUpload={() => false}>
|
||||||
|
<Input type="file" />
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Loading */}
|
||||||
|
{
|
||||||
|
loadingState && <div style={{
|
||||||
|
zIndex:"99999999"
|
||||||
|
}} className="absolute inset-0 flex items-center justify-center bg-white/60 backdrop-blur-sm z-50">
|
||||||
|
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
136
src/pages/admin/Product/ProductManagement.tsx
Normal file
136
src/pages/admin/Product/ProductManagement.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import type { Product } from '../../../types/product.type'
|
||||||
|
import { Apis } from '../../../apis'
|
||||||
|
import { Button, Space, Table } from 'antd'
|
||||||
|
import { Outlet, useNavigate } from 'react-router'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import type { StoreType } from '../../../stores'
|
||||||
|
import type { Category } from '../../../types/category.type'
|
||||||
|
|
||||||
|
export default function ProductManagement() {
|
||||||
|
const [categoryList, setCategoryList] = useState<Category[]>([])
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [productList, setProductList] = useState<Product[]>([])
|
||||||
|
const [selectProduct, setSelectProduct] = useState<Product>(null)
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchProductList() {
|
||||||
|
try {
|
||||||
|
await Apis.category.getAll()
|
||||||
|
.then(res => {
|
||||||
|
setCategoryList(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
let result = await Apis.product.findAll()
|
||||||
|
setProductList(result)
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProductList()
|
||||||
|
}, [])
|
||||||
|
const columns = [
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Số Thứ Tự',
|
||||||
|
key: 'index',
|
||||||
|
render: (_: any, __: any, index: number) => index + 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tên sản phẩm',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Giá sản phẩm',
|
||||||
|
dataIndex: 'price',
|
||||||
|
key: 'price',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tên danh mục',
|
||||||
|
dataIndex: 'categoryId',
|
||||||
|
key: 'categoryId',
|
||||||
|
render: (_: any, record: Product) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<span>{categoryList.find((item) => {
|
||||||
|
return item.id == record.categoryId
|
||||||
|
})?.title || "Unknow"}</span>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Icon',
|
||||||
|
dataIndex: 'iconUrl',
|
||||||
|
key: 'iconUrl',
|
||||||
|
render: (_: any, record: Product) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<img src={record.iconUrl} style={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
borderRadius: "50"
|
||||||
|
}} />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Trạng Thái',
|
||||||
|
dataIndex: 'isActive',
|
||||||
|
key: 'isActive',
|
||||||
|
render: (_: any, record: Product) => (
|
||||||
|
<Space size="middle">
|
||||||
|
{
|
||||||
|
record.isActive ?
|
||||||
|
<Button color="danger" variant="solid">Ngừng Hoạt Động</Button>
|
||||||
|
: <Button type='primary'>Kích Hoạt</Button>
|
||||||
|
}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Mô Tả',
|
||||||
|
dataIndex: 'isActive',
|
||||||
|
key: 'isActive',
|
||||||
|
render: (_: any, record: Product) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Button onClick={() => {
|
||||||
|
setSelectProduct(record)
|
||||||
|
}}>Xem Mô Tả</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Công Cụ',
|
||||||
|
dataIndex: 'isActive',
|
||||||
|
key: 'isActive',
|
||||||
|
render: (_: any, record: Product) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Button color="danger" variant="solid">Xóa</Button>
|
||||||
|
<Button type='primary'>Sửa</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
selectProduct && <div className='des_ox' dangerouslySetInnerHTML={{ __html: selectProduct.des }}></div>
|
||||||
|
}
|
||||||
|
<h1>ProductManagement</h1>
|
||||||
|
<Button onClick={() => {
|
||||||
|
navigate("add")
|
||||||
|
}} type='primary'>Add New Product</Button>
|
||||||
|
<Outlet></Outlet>
|
||||||
|
<Table
|
||||||
|
dataSource={productList}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
149
src/pages/admin/Product/components/CreateProductForm.tsx
Normal file
149
src/pages/admin/Product/components/CreateProductForm.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { Editor } from '@tinymce/tinymce-react';
|
||||||
|
import type { Category } from '../../../../types/category.type';
|
||||||
|
import { Apis } from '../../../../apis';
|
||||||
|
import { Button, Form, Input, InputNumber, Select, Upload } from 'antd';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { loadingAction } from '../../../../stores/slices/loading.slice';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function CreateProductForm() {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [categoryList, setCategoryList] = useState<Category[]>([])
|
||||||
|
|
||||||
|
const [apiLoading, setApiLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Apis.category.getAll()
|
||||||
|
.then(res => {
|
||||||
|
setCategoryList(res)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
const handleFinish = async (values: any) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const file = values.iconUrl.file;
|
||||||
|
if (!file) {
|
||||||
|
alert("phải chọn hình")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let iconUrl = await Apis.cloundInary.upload(file)
|
||||||
|
const data = {
|
||||||
|
...values,
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
des: editorRef.current?.getContent()
|
||||||
|
}
|
||||||
|
console.log('data', data)
|
||||||
|
|
||||||
|
let result = await Apis.product.create(data);
|
||||||
|
window.location.href = "/admin/product"
|
||||||
|
|
||||||
|
} catch (er) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
border: "1px solid black"
|
||||||
|
}}>
|
||||||
|
<h1>Thêm sản phẩm</h1>
|
||||||
|
{
|
||||||
|
apiLoading ? <>Đang thêm ....</> : <>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleFinish}
|
||||||
|
style={{ maxWidth: 500, margin: 'auto' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Tên sản phẩm"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: 'Vui lòng nhập tên sản phẩm!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Nhập tên sản phẩm" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Giá sản phẩm"
|
||||||
|
name="price"
|
||||||
|
rules={[{ required: true, message: 'Vui lòng nhập giá sản phẩm!' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Nhập giá sản phẩm"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Hình ảnh sản phẩm"
|
||||||
|
name="iconUrl"
|
||||||
|
valuePropName="file"
|
||||||
|
getValueFromEvent={(e) => e}
|
||||||
|
rules={[{ required: true, message: 'Vui lòng chọn hình ảnh!' }]}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
beforeUpload={() => false} // chặn upload tự động, chỉ lấy file
|
||||||
|
maxCount={1}
|
||||||
|
listType="picture"
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />}>Chọn ảnh</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Danh mục"
|
||||||
|
name="categoryId"
|
||||||
|
rules={[{ required: true, message: 'Vui lòng chọn danh mục!' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="Chọn danh mục">
|
||||||
|
{categoryList.map((item) => (
|
||||||
|
<Select.Option key={item.id} value={item.id}>
|
||||||
|
{item.title}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" block>
|
||||||
|
Thêm sản phẩm
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<p>Mô Tả Sản Phẩm</p>
|
||||||
|
<Editor
|
||||||
|
apiKey='gl284vy5pkwj6d6jdt9r7n6z7398b9gmowka4onfdqr1wq6h'
|
||||||
|
onInit={(_evt, editor) => editorRef.current = editor}
|
||||||
|
initialValue="<p>This is the initial content of the editor.</p>"
|
||||||
|
init={{
|
||||||
|
height: 500,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
|
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||||
|
],
|
||||||
|
toolbar: 'undo redo | blocks | ' +
|
||||||
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
|
'removeformat | help',
|
||||||
|
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export default function ProtectedAdmin(
|
|||||||
return store.user
|
return store.user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* Đã đăng nhập và là master hoặc admin */
|
||||||
if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) {
|
if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -23,7 +23,8 @@ export default function ProtectedAdmin(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userStore.data?.role) {
|
/* chưa đăng nhập */
|
||||||
|
if (!userStore.data && !userStore.loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4">
|
||||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden">
|
<div className="w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
@@ -90,11 +91,16 @@ export default function ProtectedAdmin(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* đã đăng nhập nhưng không phải master hoặc admin */
|
||||||
if (userStore.data?.role) {
|
if (userStore.data?.role) {
|
||||||
window.location.href="/"
|
window.location.href = "/"
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* chưa vào case, default */
|
||||||
|
|
||||||
|
|
||||||
async function signInHandle(e: FormEvent) {
|
async function signInHandle(e: FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let data: UserSignInDTO = {
|
let data: UserSignInDTO = {
|
||||||
@@ -103,10 +109,9 @@ export default function ProtectedAdmin(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let result = await Apis.user.signIn(data)
|
let result = await Apis.user.signIn(data)
|
||||||
localStorage.setItem("userLogin", JSON.stringify(result.data))
|
localStorage.setItem("token", result)
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "Đăng nhập thành công",
|
title: "Đăng nhập thành công",
|
||||||
content: result.message,
|
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function HeaderCom({ collapsed, setCollapsed }: { collapsed: bool
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
localStorage.removeItem("userLogin")
|
localStorage.removeItem("token")
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}}>logout</Button>
|
}}>logout</Button>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ export default function Slider({ collapsed }: { collapsed: boolean }) {
|
|||||||
key: 'user',
|
key: 'user',
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
label: "Quản lý người dùng",
|
label: "Quản lý người dùng",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: "Quản lý danh mục",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'product',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: "Quản lý sản phẩm",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ export default function Auth() {
|
|||||||
console.log("loginData", loginData)
|
console.log("loginData", loginData)
|
||||||
try {
|
try {
|
||||||
let data = await Apis.user.signIn(loginData)
|
let data = await Apis.user.signIn(loginData)
|
||||||
localStorage.setItem("userLogin", data.data.id)
|
localStorage.setItem("token", data)
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `Chào mừng ${data.data.userName} đã quay trở lại`,
|
title: `Chào mừng bạn đã quay trở lại`,
|
||||||
content: ``,
|
content: ``,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function Header() {
|
|||||||
window.location.href = "/admin"
|
window.location.href = "/admin"
|
||||||
}} className="fa-solid fa-lock"></i>}
|
}} className="fa-solid fa-lock"></i>}
|
||||||
<i onClick={() => {
|
<i onClick={() => {
|
||||||
window.localStorage.removeItem("userLogin")
|
window.localStorage.removeItem("token")
|
||||||
window.location.href = "/auth"
|
window.location.href = "/auth"
|
||||||
}} className="cursor-pointer fa-solid fa-right-from-bracket"></i>
|
}} className="cursor-pointer fa-solid fa-right-from-bracket"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||||
import { userAction, userReducer } from "./slices/user.slice";
|
import { userAction, userReducer } from "./slices/user.slice";
|
||||||
|
import { loadingReducer } from "./slices/loading.slice";
|
||||||
|
|
||||||
|
|
||||||
const RootReducer = combineReducers({
|
const RootReducer = combineReducers({
|
||||||
user: userReducer
|
user: userReducer,
|
||||||
|
loading: loadingReducer
|
||||||
})
|
})
|
||||||
|
|
||||||
export type StoreType = ReturnType<typeof RootReducer>
|
export type StoreType = ReturnType<typeof RootReducer>
|
||||||
|
|||||||
14
src/stores/slices/loading.slice.ts
Normal file
14
src/stores/slices/loading.slice.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const loadingSlice = createSlice({
|
||||||
|
name: "loading",
|
||||||
|
initialState: false,
|
||||||
|
reducers: {
|
||||||
|
change: (state) => {
|
||||||
|
state = !state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const loadingReducer = loadingSlice.reducer
|
||||||
|
export const loadingAction = loadingSlice.actions
|
||||||
@@ -21,8 +21,15 @@ const userSlice = createSlice({
|
|||||||
|
|
||||||
},
|
},
|
||||||
extraReducers: (bd) => {
|
extraReducers: (bd) => {
|
||||||
|
bd.addCase(fetchUserData.pending, (state, action) => {
|
||||||
|
state.loading = true
|
||||||
|
})
|
||||||
bd.addCase(fetchUserData.fulfilled, (state, action) => {
|
bd.addCase(fetchUserData.fulfilled, (state, action) => {
|
||||||
state.data = action.payload
|
state.loading = false
|
||||||
|
state.data = action.payload
|
||||||
|
})
|
||||||
|
bd.addCase(fetchUserData.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -31,8 +38,8 @@ const userSlice = createSlice({
|
|||||||
const fetchUserData = createAsyncThunk(
|
const fetchUserData = createAsyncThunk(
|
||||||
"user/fetchUserData",
|
"user/fetchUserData",
|
||||||
async () => {
|
async () => {
|
||||||
let result = await Apis.user.findById(localStorage.getItem("userLogin"))
|
let result = await Apis.user.me(localStorage.getItem("token")) as any
|
||||||
return result.data
|
return result
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
6
src/types/category.type.ts
Normal file
6
src/types/category.type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Category {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
iconUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
9
src/types/product.type.ts
Normal file
9
src/types/product.type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
categoryId: string;
|
||||||
|
price: number;
|
||||||
|
iconUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
name: string;
|
||||||
|
des: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user