2 Commits

Author SHA1 Message Date
1a1fb7b67b temp 2 2025-10-08 07:44:27 +07:00
4df6ba7843 temp 2025-10-08 07:37:43 +07:00
13 changed files with 8 additions and 306 deletions

5
.env
View File

@@ -1,7 +1,2 @@
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"

View File

@@ -1,4 +1,3 @@
(Video)[https://rikkeieducation.sg.larksuite.com/minutes/obsgcl8da4w9x316w6jae174]
## Rikkei Store
## Bán máy tính & điện thoại
@@ -15,9 +14,3 @@
- Đăng ký / đăng nhập X
- Layout người dùng X
## Cloudinary
npm i cloudinary
## Update test

44
db.json
View File

@@ -33,49 +33,5 @@
"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
}
]
}

31
package-lock.json generated
View File

@@ -13,7 +13,6 @@
"@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",
@@ -3178,19 +3177,6 @@
"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",
@@ -4563,12 +4549,6 @@
"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",
@@ -4942,17 +4922,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": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@@ -16,7 +16,6 @@
"@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",

View File

@@ -5,19 +5,18 @@ 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='category' element={<CategoryManagement />}></Route>
<Route path='user' element={<UserManagement/>}></Route>
</Route>
</Routes>
}

View File

@@ -1,20 +0,0 @@
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
}
}

View File

@@ -1,12 +0,0 @@
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
}
}

View File

@@ -1,9 +1,5 @@
import { CategoryApi } from "./core/category.api";
import { CloudianryApi } from "./core/cloudinary.api";
import { UserApi } from "./core/user.api";
export const Apis = {
user: UserApi,
category: CategoryApi,
cloundInary: CloudianryApi
user: UserApi
}

View File

@@ -1,163 +0,0 @@
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 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>
)
}

View File

@@ -13,11 +13,6 @@ 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",
}
]

View File

@@ -25,6 +25,7 @@ const userSlice = createSlice({
state.loading = true
})
bd.addCase(fetchUserData.fulfilled, (state, action) => {
console.log("đã vào full", action.payload)
state.loading = false
state.data = action.payload
})

View File

@@ -1,6 +0,0 @@
export interface Category {
id: string;
title: string;
iconUrl: string;
isActive: boolean;
}