category management, upload cloudinary
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
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