From c17f0d3d9cefe337223ef1669eb95716640c3150 Mon Sep 17 00:00:00 2001 From: PhuocNTB Date: Wed, 8 Oct 2025 08:54:30 +0700 Subject: [PATCH] category management, upload cloudinary --- .env | 7 +- README.md | 4 + db.json | 44 +++++ package-lock.json | 31 ++++ package.json | 1 + src/RouterSetup.tsx | 11 +- src/apis/core/category.api.ts | 20 +++ src/apis/core/cloudinary.api.ts | 12 ++ src/apis/index.ts | 6 +- .../admin/Category/CategoryManagement.tsx | 163 ++++++++++++++++++ src/pages/admin/components/Slider.tsx | 5 + src/stores/slices/user.slice.ts | 1 - src/types/category.type.ts | 6 + 13 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 src/apis/core/category.api.ts create mode 100644 src/apis/core/cloudinary.api.ts create mode 100644 src/pages/admin/Category/CategoryManagement.tsx create mode 100644 src/types/category.type.ts diff --git a/.env b/.env index a4fb8f9..9c830d3 100644 --- a/.env +++ b/.env @@ -1,2 +1,7 @@ VITE_SV_HOST="http://localhost:3000" -VITE_JWT_TOKEN="phuocntbasdasasdasd" \ No newline at end of file +VITE_JWT_TOKEN="phuocntbasdasasdasd" + +VITE_CLOUDINARY_UPLOAD_PRESET="testpreset" +VITE_CLOUDINARY_CLOUD_NAME="dusw32tsq" +VITE_CLOUDINARY_API_SECRET="NeUuUfbjlZSSyq0Zf390LECRdcI" +VITE_CLOUDINARY_API_KEY="114544946391646" diff --git a/README.md b/README.md index edb86ac..ad919f6 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,7 @@ - Đăng ký / đăng nhập X - Layout người dùng X + + +## Cloudinary +npm i cloudinary \ No newline at end of file diff --git a/db.json b/db.json index 905dd14..3cc6cc1 100644 --- a/db.json +++ b/db.json @@ -33,5 +33,49 @@ "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 + } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eeb2682..63a7899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@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", @@ -3177,6 +3178,19 @@ "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", @@ -4549,6 +4563,12 @@ "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", @@ -4922,6 +4942,17 @@ "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", diff --git a/package.json b/package.json index ff8cdfd..e45936d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@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", diff --git a/src/RouterSetup.tsx b/src/RouterSetup.tsx index 675c6ec..5b4e4c9 100644 --- a/src/RouterSetup.tsx +++ b/src/RouterSetup.tsx @@ -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 - }> - }> + }> + }> - + }> - }> - + }> + }> } diff --git a/src/apis/core/category.api.ts b/src/apis/core/category.api.ts new file mode 100644 index 0000000..d3cd71a --- /dev/null +++ b/src/apis/core/category.api.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/apis/core/cloudinary.api.ts b/src/apis/core/cloudinary.api.ts new file mode 100644 index 0000000..eb4b752 --- /dev/null +++ b/src/apis/core/cloudinary.api.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/apis/index.ts b/src/apis/index.ts index 95f7b9b..557f8ef 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -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 } \ No newline at end of file diff --git a/src/pages/admin/Category/CategoryManagement.tsx b/src/pages/admin/Category/CategoryManagement.tsx new file mode 100644 index 0000000..e06df03 --- /dev/null +++ b/src/pages/admin/Category/CategoryManagement.tsx @@ -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([]) + + 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) => ( + + + + ) + }, + { + title: 'Trạng Thái', + dataIndex: 'isActive', + key: 'isActive', + render: (_: any, record: Category) => ( + + { + record.isActive ? + + : + } + + ) + }, + { + title: 'Công Cụ', + dataIndex: 'isActive', + key: 'isActive', + render: (_: any, record: Category) => ( + + + + + ) + } + ]; + + /* form */ + const [form] = Form.useForm() + return ( +
+

Quản Lý Danh Mục

+ + + {/* Modal Create new */} + { + 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" + }} + > +
+ + + + + + false}> + + + + +
+ + {/* Loading */} + { + loadingState &&
+
+
+ } + + ) +} diff --git a/src/pages/admin/components/Slider.tsx b/src/pages/admin/components/Slider.tsx index f4b96dd..bc72431 100644 --- a/src/pages/admin/components/Slider.tsx +++ b/src/pages/admin/components/Slider.tsx @@ -13,6 +13,11 @@ export default function Slider({ collapsed }: { collapsed: boolean }) { key: 'user', icon: , label: "Quản lý người dùng", + }, + { + key: 'category', + icon: , + label: "Quản lý danh mục", } ] diff --git a/src/stores/slices/user.slice.ts b/src/stores/slices/user.slice.ts index ad5992d..892210c 100644 --- a/src/stores/slices/user.slice.ts +++ b/src/stores/slices/user.slice.ts @@ -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 }) diff --git a/src/types/category.type.ts b/src/types/category.type.ts new file mode 100644 index 0000000..2d75221 --- /dev/null +++ b/src/types/category.type.ts @@ -0,0 +1,6 @@ +export interface Category { + id: string; + title: string; + iconUrl: string; + isActive: boolean; +} \ No newline at end of file