Compare commits
4 Commits
7645838eb8
...
update/rea
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
## Bán máy tính & điện thoại
|
||||
|
||||
@@ -14,3 +15,9 @@
|
||||
- Đăng ký / đăng nhập X
|
||||
- Layout người dùng X
|
||||
|
||||
|
||||
|
||||
## Cloudinary
|
||||
npm i cloudinary
|
||||
|
||||
## Update test
|
||||
44
db.json
44
db.json
@@ -33,5 +33,49 @@
|
||||
"status": "ACTIVE",
|
||||
"banReason": ""
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "aa",
|
||||
"title": "danh mục 1",
|
||||
"iconUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTLx0GLJkUHO42P1w4qBVzajal81_-N7oP-6w&s",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "bb",
|
||||
"title": "danh mục 2",
|
||||
"iconUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTLx0GLJkUHO42P1w4qBVzajal81_-N7oP-6w&s",
|
||||
"isActive": false
|
||||
},
|
||||
{
|
||||
"id": "cafa",
|
||||
"title": "Danh mục 3",
|
||||
"iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759887991/gtsjxafutf5nu140skar.png",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "20d1",
|
||||
"title": "Điện Thoại",
|
||||
"iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759888235/mehgtcrul6kmfp9oklko.png",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "545a",
|
||||
"title": "Mèo",
|
||||
"iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759888284/ewzw5idmjosvqzk4c2cn.png",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "37f1",
|
||||
"title": "Mèo 12121",
|
||||
"iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759888337/whofwib0nrquknf4v5b6.png",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "2d29",
|
||||
"title": "Mèo 12121",
|
||||
"iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759888364/oululvqiwuwpn8se3dww.png",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -13,6 +13,8 @@
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"antd": "^5.27.4",
|
||||
"axios": "^1.12.2",
|
||||
"cloudinary": "^2.7.0",
|
||||
"jose": "^6.1.0",
|
||||
"json-server": "^1.0.0-beta.3",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
@@ -3176,6 +3178,19 @@
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -4157,6 +4172,15 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4539,6 +4563,12 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -4912,6 +4942,17 @@
|
||||
"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": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"antd": "^5.27.4",
|
||||
"axios": "^1.12.2",
|
||||
"cloudinary": "^2.7.0",
|
||||
"jose": "^6.1.0",
|
||||
"json-server": "^1.0.0-beta.3",
|
||||
"react": "^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 RouterSetup from './RouterSetup'
|
||||
import { useSelector } from 'react-redux'
|
||||
import type { StoreType } from './stores'
|
||||
import Loading from './components/Loading'
|
||||
|
||||
export default function App() {
|
||||
|
||||
const userStore = useSelector((store: StoreType) => store.user)
|
||||
|
||||
return (
|
||||
<RouterSetup />
|
||||
<>
|
||||
{
|
||||
userStore.loading ? <Loading />
|
||||
: <RouterSetup />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,18 +5,19 @@ import Admin from './pages/admin/Admin'
|
||||
import ProtectedAdmin from './pages/admin/auth/ProtectedAdmin'
|
||||
import Auth from './pages/home/auth/Auth'
|
||||
import UserManagement from './pages/admin/User/UserManagement'
|
||||
import CategoryManagement from './pages/admin/Category/CategoryManagement'
|
||||
|
||||
export default function RouterSetup() {
|
||||
return <Routes>
|
||||
<Route path='/' element={<Home/>}></Route>
|
||||
<Route path='/auth' element={<Auth/>}></Route>
|
||||
<Route path='/' element={<Home />}></Route>
|
||||
<Route path='/auth' element={<Auth />}></Route>
|
||||
<Route path='admin' element={
|
||||
<ProtectedAdmin>
|
||||
<Admin/>
|
||||
<Admin />
|
||||
</ProtectedAdmin>
|
||||
}>
|
||||
<Route path='user' element={<UserManagement/>}></Route>
|
||||
|
||||
<Route path='user' element={<UserManagement />}></Route>
|
||||
<Route path='category' element={<CategoryManagement />}></Route>
|
||||
</Route>
|
||||
</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
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import axios from "axios"
|
||||
import type { User } from "../../types/user.type"
|
||||
import { message } from "antd"
|
||||
import { ApiUtil } from "../../utils/api.util"
|
||||
import * as jose from 'jose'
|
||||
|
||||
export interface UserSignInDTO {
|
||||
emailOrUserName: string
|
||||
@@ -16,10 +17,7 @@ export interface UserFindAllDTO {
|
||||
}
|
||||
|
||||
export const UserApi = {
|
||||
signIn: async (data: UserSignInDTO): Promise<{
|
||||
message: string
|
||||
data: any
|
||||
}> => {
|
||||
signIn: async (data: UserSignInDTO) => {
|
||||
let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`)
|
||||
if (userData.data.length == 0) {
|
||||
userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`)
|
||||
@@ -36,10 +34,7 @@ export const UserApi = {
|
||||
data: null
|
||||
})
|
||||
}
|
||||
return {
|
||||
message: "Đăng nhập thành công!",
|
||||
data: userData.data[0] as User
|
||||
}
|
||||
return createToken(userData.data[0].id)
|
||||
}
|
||||
},
|
||||
signUp: async (data: User) => {
|
||||
@@ -106,5 +101,59 @@ export const UserApi = {
|
||||
findAll: async (query?: UserFindAllDTO) => {
|
||||
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query))
|
||||
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,9 @@
|
||||
import { CategoryApi } from "./core/category.api";
|
||||
import { CloudianryApi } from "./core/cloudinary.api";
|
||||
import { UserApi } from "./core/user.api";
|
||||
|
||||
export const Apis = {
|
||||
user: UserApi
|
||||
user: UserApi,
|
||||
category: CategoryApi,
|
||||
cloundInary: CloudianryApi
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export default function ProtectedAdmin(
|
||||
return store.user
|
||||
})
|
||||
|
||||
|
||||
/* Đã đăng nhập và là master hoặc admin */
|
||||
if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) {
|
||||
return (
|
||||
<>
|
||||
@@ -23,7 +23,8 @@ export default function ProtectedAdmin(
|
||||
)
|
||||
}
|
||||
|
||||
if (!userStore.data?.role) {
|
||||
/* chưa đăng nhập */
|
||||
if (!userStore.data && !userStore.loading) {
|
||||
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="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) {
|
||||
window.location.href="/"
|
||||
window.location.href = "/"
|
||||
return <></>
|
||||
}
|
||||
|
||||
|
||||
/* chưa vào case, default */
|
||||
|
||||
|
||||
async function signInHandle(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
let data: UserSignInDTO = {
|
||||
@@ -103,10 +109,9 @@ export default function ProtectedAdmin(
|
||||
}
|
||||
try {
|
||||
let result = await Apis.user.signIn(data)
|
||||
localStorage.setItem("userLogin", JSON.stringify(result.data))
|
||||
localStorage.setItem("token", result)
|
||||
Modal.confirm({
|
||||
title: "Đăng nhập thành công",
|
||||
content: result.message,
|
||||
onOk: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function HeaderCom({ collapsed, setCollapsed }: { collapsed: bool
|
||||
</select>
|
||||
|
||||
<Button onClick={() => {
|
||||
localStorage.removeItem("userLogin")
|
||||
localStorage.removeItem("token")
|
||||
window.location.reload()
|
||||
}}>logout</Button>
|
||||
</Header>
|
||||
|
||||
@@ -13,6 +13,11 @@ export default function Slider({ collapsed }: { collapsed: boolean }) {
|
||||
key: 'user',
|
||||
icon: <UserOutlined />,
|
||||
label: "Quản lý người dùng",
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
icon: <UserOutlined />,
|
||||
label: "Quản lý danh mục",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@ export default function Auth() {
|
||||
console.log("loginData", loginData)
|
||||
try {
|
||||
let data = await Apis.user.signIn(loginData)
|
||||
localStorage.setItem("userLogin", data.data.id)
|
||||
localStorage.setItem("token", data)
|
||||
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: ``,
|
||||
onOk: () => {
|
||||
window.location.href = "/"
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function Header() {
|
||||
window.location.href = "/admin"
|
||||
}} className="fa-solid fa-lock"></i>}
|
||||
<i onClick={() => {
|
||||
window.localStorage.removeItem("userLogin")
|
||||
window.localStorage.removeItem("token")
|
||||
window.location.href = "/auth"
|
||||
}} className="cursor-pointer fa-solid fa-right-from-bracket"></i>
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,15 @@ const userSlice = createSlice({
|
||||
|
||||
},
|
||||
extraReducers: (bd) => {
|
||||
bd.addCase(fetchUserData.pending, (state, action) => {
|
||||
state.loading = true
|
||||
})
|
||||
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(
|
||||
"user/fetchUserData",
|
||||
async () => {
|
||||
let result = await Apis.user.findById(localStorage.getItem("userLogin"))
|
||||
return result.data
|
||||
let result = await Apis.user.me(localStorage.getItem("token")) as any
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user