Compare commits
4 Commits
b74ef6a892
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c75ab264 | |||
| 3a32b0fecc | |||
| 479b57cd1e | |||
| fe093ab8dc |
@@ -19,3 +19,5 @@
|
|||||||
|
|
||||||
## Cloudinary
|
## Cloudinary
|
||||||
npm i cloudinary
|
npm i cloudinary
|
||||||
|
|
||||||
|
## Update test
|
||||||
60
package-lock.json
generated
60
package-lock.json
generated
@@ -8,9 +8,11 @@
|
|||||||
"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",
|
"cloudinary": "^2.7.0",
|
||||||
@@ -2463,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",
|
||||||
@@ -4185,7 +4206,6 @@
|
|||||||
"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": {
|
||||||
@@ -4576,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",
|
||||||
@@ -4786,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",
|
||||||
@@ -4926,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",
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
"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",
|
"cloudinary": "^2.7.0",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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 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>
|
||||||
@@ -18,6 +20,9 @@ export default function RouterSetup() {
|
|||||||
}>
|
}>
|
||||||
<Route path='user' element={<UserManagement />}></Route>
|
<Route path='user' element={<UserManagement />}></Route>
|
||||||
<Route path='category' element={<CategoryManagement />}></Route>
|
<Route path='category' element={<CategoryManagement />}></Route>
|
||||||
|
<Route path='product' element={<ProductManagement />}>
|
||||||
|
<Route path='add' element={<CreateProductForm />}></Route>
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { CategoryApi } from "./core/category.api";
|
import { CategoryApi } from "./core/category.api";
|
||||||
import { CloudianryApi } from "./core/cloudinary.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,
|
category: CategoryApi,
|
||||||
cloundInary: CloudianryApi
|
cloundInary: CloudianryApi,
|
||||||
|
product: ProductApi
|
||||||
}
|
}
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ export default function Slider({ collapsed }: { collapsed: boolean }) {
|
|||||||
key: 'category',
|
key: 'category',
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
label: "Quản lý danh mục",
|
label: "Quản lý danh mục",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'product',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: "Quản lý sản phẩm",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
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