category management, upload cloudinary

This commit is contained in:
2025-10-08 08:54:30 +07:00
parent 21e77456c9
commit c17f0d3d9c
13 changed files with 303 additions and 8 deletions

View File

@@ -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>
}

View 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
}
}

View 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
}
}

View File

@@ -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
}

View 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 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,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",
}
]

View File

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

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