diff --git a/db.json b/db.json index 8529753..1b8bd90 100644 --- a/db.json +++ b/db.json @@ -46,36 +46,6 @@ "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 } ], "products": [ @@ -89,92 +59,27 @@ "des": "asasasa" }, { - "id": "f52d", - "name": "Sản Phẩm Mới", - "price": 10000, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972232/vako1ivmnk8oqfjvxu5e.png", - "categoryId": "37f1", - "des": "" - }, - { - "id": "d33f", - "name": "asdas", - "price": 12312312, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972302/om1v3jypr8kilajpyjrq.png", - "categoryId": "bb", - "des": "" - }, - { - "id": "89fb", - "name": "asdas", - "price": 13212312, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972392/xkighdvy507fbpcuwvft.png", - "categoryId": "20d1", - "des": "", + "id": "d317", + "name": "Sản Phẩm Vip", + "price": 100000, + "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759976982/z7kus1kwxqfahi6bcwwo.png", + "categoryId": "aa", + "des": "
\n

Thông tin sản phẩm

\n
\n
\n
\n
\n

 

\n

Đánh giá chi tiết màn hình Viewsonic VA2432A-H 24\" IPS 120Hz viền mỏng

\n

Với tần số quét 120Hz và tấm nền IPS, màn hình Viewsonic VA2432A-H 24\" là một lựa chọn tuyệt vời cho cả game thủ và những người làm việc đồ họa. Chiếc màn hình này hứa hẹn sẽ đem lại cho bạn những hình ảnh sinh động và mượt mà, hỗ trợ nâng cao năng suất hoạt động.

\n

Hình ảnh sắc nét với tần số quét 120Hz, tốc độ phản hồi 1ms

\n

Tần số quét 120Hz của màn hình ViewSonic VA2432A-H giúp hình ảnh chuyển động mượt mà hơn gấp đôi so với màn hình máy tính 60Hz thông thường. Bạn sẽ không còn bỏ lỡ bất kỳ chi tiết nào trong những pha hành động nhanh của game FPS. Song song đó với thời gian phản hồi 1ms (MPRT) siêu nhanh giúp loại bỏ hiện tượng bóng mờ, mang đến trải nghiệm chơi game mượt mà và không bị giật lag.

\n

\"Màn

\n

Ngoại hình hiện đại, tinh tế với ba cạnh không viền

\n

Màn hình Viewsonic VA2432A-H có thiết kế hiện đại và tối giản với viền màn hình  siêu mỏng, tạo cảm giác màn hình tràn viền, giúp bạn tập trung vào nội dung hiển thị mà không bị phân tán bởi các chi tiết thừa. Phần chân đế thường được làm từ chất liệu nhựa cao cấp mang lại cảm giác chắc chắn và bền bỉ.

\n
\n
\n
", "isActive": true + } + ], + "carts": [ + { + "id": "a", + "userId": "1", + "productId": "aaa", + "quantity": 11 }, { - "id": "293d", - "name": "asdas", - "price": 13212312, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972446/cykgd7rr2biamil4wmbp.png", - "categoryId": "20d1", - "des": "

This is the initial content of the easdasdasditor.

", - "isActive": true - }, - { - "id": "d14d", - "name": "asdas131231", - "price": 13212312, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972577/qhcajdypn0hkbpj8wngn.png", - "categoryId": "37f1", - "des": "

This is the initial content of the easdasdasditor.adasdasdasd

\n

asdasd

\n

asdas

\n

asdas

\n

asda

\n

 

\n

 

\n

", - "isActive": true - }, - { - "id": "0f28", - "name": "Yasdasdassd", - "price": 121212, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972822/vuwgc9qc4ibywllovgfi.png", - "categoryId": "37f1", - "des": "

This is the initial content of the editor.

", - "isActive": true - }, - { - "id": "1d6b", - "name": "Yasdasdassd", - "price": 121212, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972847/tyb4pyhwre1mhwcean5v.png", - "categoryId": "37f1", - "des": "

This is the initial content of the editor.

", - "isActive": true - }, - { - "id": "e718", - "name": "Yasdasdassd", - "price": 121212, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759972882/f1ycf1xtlevvo4jwqrwh.png", - "categoryId": "37f1", - "des": "

This is the initial content of the editor.

", - "isActive": true - }, - { - "id": "b4c9", - "name": "assda", - "price": 1231231, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759973036/zf8euabhj88wvgqpa1n4.png", - "categoryId": "bb", - "des": "

This is the initial content of the editor.

\n

 

\n

", - "isActive": true - }, - { - "id": "8d17", - "name": "s1231 56786786", - "price": 12321312, - "iconUrl": "https://res.cloudinary.com/dusw32tsq/image/upload/v1759973083/whwafvfupldclzjxn9bs.png", - "categoryId": "545a", - "des": "

This is the initial content of the editor.

\n

", - "isActive": true + "id": "c06c", + "productId": "d317", + "userId": "1", + "quantity": 48 } ] } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index e9a6ca8..755225d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,27 @@ -import { createContext } from 'react' +import { createContext, useEffect } from 'react' import RouterSetup from './RouterSetup' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import type { StoreType } from './stores' import Loading from './components/Loading' +import { Apis } from './apis' +import { userAction } from './stores/slices/user.slice' export default function App() { const userStore = useSelector((store: StoreType) => store.user) + const dipatch = useDispatch() + useEffect(() => { + if(userStore.data && !userStore.loading) { + try { + Apis.cart.getCartByUserId(userStore.data.id) + .then(res => { + dipatch(userAction.initCartData(res)) + }) + }catch(err) { + + } + } + }, [userStore.data, userStore.loading, userStore.reloadCart]) return ( <> { diff --git a/src/RouterSetup.tsx b/src/RouterSetup.tsx index 30796bb..cb9be1e 100644 --- a/src/RouterSetup.tsx +++ b/src/RouterSetup.tsx @@ -8,10 +8,19 @@ 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' +import Cart from './pages/home/cart/Cart' +import HomeContent from './pages/home/HomeContent' +import Collection from './pages/home/collections/Collection' +import ProductDetail from './pages/home/product/ProductDetail' export default function RouterSetup() { return - }> + }> + }> + }> + }> + }> + }> diff --git a/src/apis/core/cart.api.ts b/src/apis/core/cart.api.ts new file mode 100644 index 0000000..1ecd369 --- /dev/null +++ b/src/apis/core/cart.api.ts @@ -0,0 +1,32 @@ +import axios from "axios"; + +export interface AddToCartDTO { + userId: string; + productId: string; +} + +export const CartApi = { + getCartByUserId: async (userId: string) => { + let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/carts?userId=${userId}`) + return result.data + }, + addToCart: async (data: AddToCartDTO) => { + let resultExisted = await axios.get(`${import.meta.env.VITE_SV_HOST}/carts?productId=${data.productId}`) + if (!resultExisted.data[0]) { + /* thêm mới */ + let addRes = await axios.post(`${import.meta.env.VITE_SV_HOST}/carts`, { + ...data, + quantity: 1 + }) + + return addRes.data + } + + /* cập nhật */ + let resultUpdate = await axios.patch(`${import.meta.env.VITE_SV_HOST}/carts/${resultExisted.data[0].id}`, { + quantity: resultExisted.data[0].quantity + 1 + }) + + return resultUpdate.data + } +} \ No newline at end of file diff --git a/src/apis/core/product.api.ts b/src/apis/core/product.api.ts index 254f2fa..0b52e74 100644 --- a/src/apis/core/product.api.ts +++ b/src/apis/core/product.api.ts @@ -19,5 +19,13 @@ export const ProductApi = { isActive: true }) return result.data - } + }, + findListWithCategoryId: async (categoryId: string) => { + let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/products?categoryId=${categoryId}`) + return result.data + }, + findProductById: async (productId: string) => { + let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/products/` + productId) + return result.data + }, } \ No newline at end of file diff --git a/src/apis/index.ts b/src/apis/index.ts index 8986f42..a91c4c8 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,3 +1,4 @@ +import { CartApi } from "./core/cart.api"; import { CategoryApi } from "./core/category.api"; import { CloudianryApi } from "./core/cloudinary.api"; import { ProductApi } from "./core/product.api"; @@ -7,5 +8,6 @@ export const Apis = { user: UserApi, category: CategoryApi, cloundInary: CloudianryApi, - product: ProductApi + product: ProductApi, + cart: CartApi } \ No newline at end of file diff --git a/src/pages/home/HomeContent.tsx b/src/pages/home/HomeContent.tsx new file mode 100644 index 0000000..e196bee --- /dev/null +++ b/src/pages/home/HomeContent.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useState } from 'react' +import type { Category } from '../../types/category.type' +import { Apis } from '../../apis' +import { Link } from 'react-router' + +export default function HomeContent() { + const [categories, setCategories] = useState([]) + + useEffect(() => { + Apis.category.getAll() + .then(res => { + setCategories(res) + }) + }, []) + return ( +
+ HomeContent +
    + { + categories.map(item => { + return ( +
  • + {item.title} +
  • + ) + }) + } +
+
+ ) +} diff --git a/src/pages/home/cart/Cart.tsx b/src/pages/home/cart/Cart.tsx new file mode 100644 index 0000000..eea7b23 --- /dev/null +++ b/src/pages/home/cart/Cart.tsx @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import type { StoreType } from '../../../stores' +import type { CartItem } from '../../../types/cart.type' +import { Apis } from '../../../apis' +import type { Product } from '../../../types/product.type' + +export default function Cart() { + const userStore = useSelector((store: StoreType) => store.user) + const [products, setProducts] = useState([]) + + // Lấy thông tin product cho từng item trong giỏ + useEffect(() => { + async function fetchProducts() { + let temsArr: Product[] = [] + for (const item of userStore.cart || []) { + const res = await Apis.product.findProductById(item.productId) + temsArr.push(res) + } + setProducts(temsArr) + } + fetchProducts() + }, [userStore.cart]) + + + + // Tính tổng tiền + const total = userStore.cart?.reduce((sum, item) => { + const p = products[item.productId] + return sum + (p ? p.price * item.quantity : 0) + }, 0) || 0 + + return ( +
+

🛒 Giỏ hàng của bạn

+ + {userStore.cart?.length === 0 ? ( +

Giỏ hàng trống.

+ ) : ( +
+
    + {userStore.cart.map((item: CartItem) => { + const product = products.find(pro => pro.id == item.productId) + return ( +
  • + {/* Ảnh sản phẩm */} +
    + {product ? ( + {product.name} + ) : ( +
    + )} +
    + + {/* Thông tin */} +
    +

    + {product ? product.name : 'Đang tải...'} +

    +

    + {product ? product.price.toLocaleString() + ' đ' : ''} +

    +
    + + {/* Số lượng & tổng */} +
    +

    SL: {item.quantity}

    + {product && ( +

    + {(product.price * item.quantity).toLocaleString()} đ +

    + )} +
    +
  • + ) + })} +
+ + {/* Tổng tiền */} +
+

+ Tổng cộng:{' '} + {total.toLocaleString()} đ +

+
+ + {/* Nút hành động */} +
+ +
+
+ )} +
+ ) +} diff --git a/src/pages/home/collections/Collection.tsx b/src/pages/home/collections/Collection.tsx new file mode 100644 index 0000000..76176b8 --- /dev/null +++ b/src/pages/home/collections/Collection.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router' +import { Apis } from '../../../apis' +import type { Product } from '../../../types/product.type' + +export default function Collection() { + const { categoryId } = useParams() + const [productList, setProductList] = useState([]) + + + useEffect(() => { + Apis.product.findListWithCategoryId((categoryId)) + .then(res => { + setProductList(res) + }) + }, [categoryId]) + return ( +
+

+ Danh sách sản phẩm (Danh mục: {categoryId}) +

+ +
+ {productList.map((item) => ( +
{ + window.location.href = "/product/" + item.id + }} + key={item.id} + className="border rounded-xl p-3 shadow hover:shadow-lg transition bg-white" + > + {item.name} +

{item.name}

+

Giá: {item.price.toLocaleString()}₫

+

+ {item.isActive ? 'Đang bán' : 'Ngừng bán'} +

+
+ ))} +
+
+ ) +} diff --git a/src/pages/home/components/Header/Header.tsx b/src/pages/home/components/Header/Header.tsx index 8234030..4fd4b46 100644 --- a/src/pages/home/components/Header/Header.tsx +++ b/src/pages/home/components/Header/Header.tsx @@ -6,6 +6,7 @@ import logo from '../../../../assets/img/logo.png' import Search from 'antd/es/input/Search' import { useSelector } from 'react-redux' import type { StoreType } from '../../../../stores' +import type { CartItem } from '../../../../types/cart.type' export default function Header() { const navigate = useNavigate() const menu = [ @@ -28,6 +29,12 @@ export default function Header() { const userStore = useSelector((store: StoreType) => { return store.user }) + + function countCartItem() { + return userStore.cart?.reduce((cur: number, next: CartItem) => { + return cur + next.quantity + }, 0) + } return (
@@ -52,9 +59,11 @@ export default function Header() { ) }) } -
+
{ + window.location.href = "/cart" + }} className='item'> -

0

+

{countCartItem()}

Giỏ

Hàng

diff --git a/src/pages/home/product/ProductDetail.tsx b/src/pages/home/product/ProductDetail.tsx new file mode 100644 index 0000000..8d53c75 --- /dev/null +++ b/src/pages/home/product/ProductDetail.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router' +import { Apis } from '../../../apis' +import type { Product } from '../../../types/product.type' +import { Button } from 'antd' +import { useDispatch, useSelector } from 'react-redux' +import type { StoreType } from '../../../stores' +import { userAction } from '../../../stores/slices/user.slice' + +export default function ProductDetail() { + const { productId } = useParams() + const [product, setProduct] = useState(null) + const userStore = useSelector((store: StoreType) => store.user) + const dipatch = useDispatch() + + useEffect(() => { + if (!productId) return + Apis.product.findProductById(productId) + .then(res => { + setProduct(res) + }) + }, [productId]) + + if (!product) { + return ( +
+
+
+ ) + } + + return ( +
+ +
+ {/* Hình ảnh sản phẩm */} +
+ {product.name} +
+ + {/* Thông tin sản phẩm */} +
+

{product.name}

+

+ {product.price.toLocaleString()} ₫ +

+ + {product.isActive ? 'Đang bán' : 'Ngừng kinh doanh'} + + +
+

Mô tả sản phẩm

+
+
+
+
+
+ ) +} diff --git a/src/stores/slices/user.slice.ts b/src/stores/slices/user.slice.ts index 892210c..668c23a 100644 --- a/src/stores/slices/user.slice.ts +++ b/src/stores/slices/user.slice.ts @@ -1,16 +1,21 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import type { User } from "../../types/user.type"; import { Apis } from "../../apis"; +import type { CartItem } from "../../types/cart.type"; interface UserState { data: User | null, loading: boolean; + cart: CartItem[], + reloadCart: boolean } const InitUserState: UserState = { data: null, - loading: false + loading: false, + cart: [], + reloadCart: false } @@ -18,7 +23,29 @@ const userSlice = createSlice({ name: "user", initialState: InitUserState, reducers: { - + initCartData: (state, action) =>{ + state.cart = action.payload + }, + changeLoad: (state) => { + state.reloadCart = !state.reloadCart + } + // addToCart: (state, action) => { + // let existedItem = state.cart.find(item => item.productId == action.payload.productId) + + // if(existedItem) { + // state.cart = state.cart.map(item => { + // if(item.id == existedItem.id) { + // return { + // ...existedItem, + // quantity: action.payload.quantity + // } + // } + // return item + // }) + // }else { + // state.cart.push(action.payload) + // } + // } }, extraReducers: (bd) => { bd.addCase(fetchUserData.pending, (state, action) => { diff --git a/src/types/cart.type.ts b/src/types/cart.type.ts new file mode 100644 index 0000000..99ce726 --- /dev/null +++ b/src/types/cart.type.ts @@ -0,0 +1,6 @@ +export interface CartItem { + id: string; + userId: string; + productId: string; + quantity: number +} \ No newline at end of file