Compare commits
2 Commits
feat/flow-
...
1a1fb7b67b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a1fb7b67b | |||
| 4df6ba7843 |
5
.env
5
.env
@@ -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"
|
||||
|
||||
@@ -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
|
||||
91
package-lock.json
generated
91
package-lock.json
generated
@@ -8,14 +8,11 @@
|
||||
"name": "react_project",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tinymce/tinymce-react": "^6.3.0",
|
||||
"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",
|
||||
@@ -2465,25 +2462,6 @@
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3199,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",
|
||||
@@ -4206,6 +4171,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -4583,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",
|
||||
@@ -4596,18 +4556,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
|
||||
@@ -4818,15 +4766,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -4967,23 +4906,6 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -5000,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",
|
||||
|
||||
@@ -11,14 +11,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tinymce/tinymce-react": "^6.3.0",
|
||||
"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",
|
||||
|
||||
@@ -5,9 +5,6 @@ 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'
|
||||
import ProductManagement from './pages/admin/Product/ProductManagement'
|
||||
import CreateProductForm from './pages/admin/Product/components/CreateProductForm'
|
||||
|
||||
export default function RouterSetup() {
|
||||
return <Routes>
|
||||
@@ -19,10 +16,7 @@ export default function RouterSetup() {
|
||||
</ProtectedAdmin>
|
||||
}>
|
||||
<Route path='user' element={<UserManagement/>}></Route>
|
||||
<Route path='category' element={<CategoryManagement />}></Route>
|
||||
<Route path='product' element={<ProductManagement />}>
|
||||
<Route path='add' element={<CreateProductForm />}></Route>
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
</Routes>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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,11 +1,5 @@
|
||||
import { CategoryApi } from "./core/category.api";
|
||||
import { CloudianryApi } from "./core/cloudinary.api";
|
||||
import { ProductApi } from "./core/product.api";
|
||||
import { UserApi } from "./core/user.api";
|
||||
|
||||
export const Apis = {
|
||||
user: UserApi,
|
||||
category: CategoryApi,
|
||||
cloundInary: CloudianryApi,
|
||||
product: ProductApi
|
||||
user: UserApi
|
||||
}
|
||||
@@ -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 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>
|
||||
)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -13,16 +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",
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
icon: <UserOutlined />,
|
||||
label: "Quản lý sản phẩm",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||
import { userAction, userReducer } from "./slices/user.slice";
|
||||
import { loadingReducer } from "./slices/loading.slice";
|
||||
|
||||
|
||||
const RootReducer = combineReducers({
|
||||
user: userReducer,
|
||||
loading: loadingReducer
|
||||
user: userReducer
|
||||
})
|
||||
|
||||
export type StoreType = ReturnType<typeof RootReducer>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
title: string;
|
||||
iconUrl: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface Product {
|
||||
id: string;
|
||||
categoryId: string;
|
||||
price: number;
|
||||
iconUrl: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
des: string;
|
||||
}
|
||||
Reference in New Issue
Block a user