Compare commits
6 Commits
82c75ab264
...
31d25529c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d25529c5 | |||
| b237658bb9 | |||
| 55e49096c5 | |||
| c746369daa | |||
| 4b11ee2180 | |||
| f7c96a0258 |
19
src/App.tsx
19
src/App.tsx
@@ -1,12 +1,27 @@
|
|||||||
import { createContext } from 'react'
|
import { createContext, useEffect } from 'react'
|
||||||
import RouterSetup from './RouterSetup'
|
import RouterSetup from './RouterSetup'
|
||||||
import { useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import type { StoreType } from './stores'
|
import type { StoreType } from './stores'
|
||||||
import Loading from './components/Loading'
|
import Loading from './components/Loading'
|
||||||
|
import { Apis } from './apis'
|
||||||
|
import { userAction } from './stores/slices/user.slice'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const userStore = useSelector((store: StoreType) => store.user)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,19 @@ 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 ProductManagement from './pages/admin/Product/ProductManagement'
|
||||||
import CreateProductForm from './pages/admin/Product/components/CreateProductForm'
|
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() {
|
export default function RouterSetup() {
|
||||||
return <Routes>
|
return <Routes>
|
||||||
<Route path='/' element={<Home />}></Route>
|
<Route path='/' element={<Home />}>
|
||||||
|
<Route path='/' element={<HomeContent />}></Route>
|
||||||
|
<Route path='cart' element={<Cart />}></Route>
|
||||||
|
<Route path='collections/:categoryId' element={<Collection />}></Route>
|
||||||
|
<Route path='product/:productId' element={<ProductDetail />}></Route>
|
||||||
|
</Route>
|
||||||
<Route path='/auth' element={<Auth />}></Route>
|
<Route path='/auth' element={<Auth />}></Route>
|
||||||
<Route path='admin' element={
|
<Route path='admin' element={
|
||||||
<ProtectedAdmin>
|
<ProtectedAdmin>
|
||||||
|
|||||||
32
src/apis/core/cart.api.ts
Normal file
32
src/apis/core/cart.api.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,5 +19,13 @@ export const ProductApi = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
})
|
})
|
||||||
return result.data
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CartApi } from "./core/cart.api";
|
||||||
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 { ProductApi } from "./core/product.api";
|
||||||
@@ -7,5 +8,6 @@ export const Apis = {
|
|||||||
user: UserApi,
|
user: UserApi,
|
||||||
category: CategoryApi,
|
category: CategoryApi,
|
||||||
cloundInary: CloudianryApi,
|
cloundInary: CloudianryApi,
|
||||||
product: ProductApi
|
product: ProductApi,
|
||||||
|
cart: CartApi
|
||||||
}
|
}
|
||||||
31
src/pages/home/HomeContent.tsx
Normal file
31
src/pages/home/HomeContent.tsx
Normal file
@@ -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<Category[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Apis.category.getAll()
|
||||||
|
.then(res => {
|
||||||
|
setCategories(res)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
HomeContent
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
categories.map(item => {
|
||||||
|
return (
|
||||||
|
<li key={item.id}>
|
||||||
|
<Link to={`/collections/${item.id}`}>{item.title}</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
104
src/pages/home/cart/Cart.tsx
Normal file
104
src/pages/home/cart/Cart.tsx
Normal file
@@ -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<Product[]>([])
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-6 text-center">🛒 Giỏ hàng của bạn</h1>
|
||||||
|
|
||||||
|
{userStore.cart?.length === 0 ? (
|
||||||
|
<p className="text-center text-gray-500">Giỏ hàng trống.</p>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white shadow-md rounded-2xl p-4">
|
||||||
|
<ul className="divide-y divide-gray-200">
|
||||||
|
{userStore.cart.map((item: CartItem) => {
|
||||||
|
const product = products.find(pro => pro.id == item.productId)
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className="flex items-center gap-4 py-4"
|
||||||
|
>
|
||||||
|
{/* Ảnh sản phẩm */}
|
||||||
|
<div className="w-16 h-16 flex-shrink-0 overflow-hidden rounded-lg border">
|
||||||
|
{product ? (
|
||||||
|
<img
|
||||||
|
src={product.iconUrl}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-gray-200 animate-pulse" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thông tin */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="font-semibold text-gray-800">
|
||||||
|
{product ? product.name : 'Đang tải...'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{product ? product.price.toLocaleString() + ' đ' : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Số lượng & tổng */}
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-700">SL: {item.quantity}</p>
|
||||||
|
{product && (
|
||||||
|
<p className="font-semibold text-gray-800">
|
||||||
|
{(product.price * item.quantity).toLocaleString()} đ
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* Tổng tiền */}
|
||||||
|
<div className="mt-6 border-t pt-4 text-right">
|
||||||
|
<p className="text-lg font-semibold">
|
||||||
|
Tổng cộng:{' '}
|
||||||
|
<span className="text-blue-600">{total.toLocaleString()} đ</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nút hành động */}
|
||||||
|
<div className="mt-6 text-right">
|
||||||
|
<button className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition">
|
||||||
|
Thanh toán
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
src/pages/home/collections/Collection.tsx
Normal file
48
src/pages/home/collections/Collection.tsx
Normal file
@@ -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<Product[]>([])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Apis.product.findListWithCategoryId((categoryId))
|
||||||
|
.then(res => {
|
||||||
|
setProductList(res)
|
||||||
|
})
|
||||||
|
}, [categoryId])
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
|
Danh sách sản phẩm (Danh mục: {categoryId})
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
{productList.map((item) => (
|
||||||
|
<div
|
||||||
|
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = "/product/" + item.id
|
||||||
|
}}
|
||||||
|
key={item.id}
|
||||||
|
className="border rounded-xl p-3 shadow hover:shadow-lg transition bg-white"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.iconUrl}
|
||||||
|
alt={item.name}
|
||||||
|
className="w-full h-40 object-cover rounded-md mb-3"
|
||||||
|
/>
|
||||||
|
<h3 className="text-lg font-semibold">{item.name}</h3>
|
||||||
|
<p className="text-gray-600">Giá: {item.price.toLocaleString()}₫</p>
|
||||||
|
<p className={`text-sm mt-1 ${item.isActive ? 'text-green-600' : 'text-red-500'}`}>
|
||||||
|
{item.isActive ? 'Đang bán' : 'Ngừng bán'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import logo from '../../../../assets/img/logo.png'
|
|||||||
import Search from 'antd/es/input/Search'
|
import Search from 'antd/es/input/Search'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import type { StoreType } from '../../../../stores'
|
import type { StoreType } from '../../../../stores'
|
||||||
|
import type { CartItem } from '../../../../types/cart.type'
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const menu = [
|
const menu = [
|
||||||
@@ -28,6 +29,12 @@ export default function Header() {
|
|||||||
const userStore = useSelector((store: StoreType) => {
|
const userStore = useSelector((store: StoreType) => {
|
||||||
return store.user
|
return store.user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function countCartItem() {
|
||||||
|
return userStore.cart?.reduce((cur: number, next: CartItem) => {
|
||||||
|
return cur + next.quantity
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
@@ -52,9 +59,11 @@ export default function Header() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
<div className='item'>
|
<div onClick={() => {
|
||||||
|
window.location.href = "/cart"
|
||||||
|
}} className='item'>
|
||||||
<i className="fa-solid fa-cart-shopping"></i>
|
<i className="fa-solid fa-cart-shopping"></i>
|
||||||
<p className='cart_count'>0</p>
|
<p className='cart_count'>{countCartItem()}</p>
|
||||||
<div className='text_box'>
|
<div className='text_box'>
|
||||||
<p>Giỏ</p>
|
<p>Giỏ</p>
|
||||||
<p>Hàng</p>
|
<p>Hàng</p>
|
||||||
|
|||||||
83
src/pages/home/product/ProductDetail.tsx
Normal file
83
src/pages/home/product/ProductDetail.tsx
Normal file
@@ -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<Product | null>(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 (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-4 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6 mt-10 bg-white shadow-lg rounded-2xl">
|
||||||
|
<Button onClick={async () => {
|
||||||
|
try {
|
||||||
|
let result = await Apis.cart.addToCart({
|
||||||
|
productId: product.id,
|
||||||
|
userId: userStore.data?.id
|
||||||
|
})
|
||||||
|
console.log("result", result)
|
||||||
|
|
||||||
|
dipatch(userAction.changeLoad())
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}}>Add To Cart</Button>
|
||||||
|
<div className="flex flex-col md:flex-row gap-6">
|
||||||
|
{/* Hình ảnh sản phẩm */}
|
||||||
|
<div className="w-full md:w-1/3 flex justify-center">
|
||||||
|
<img
|
||||||
|
src={product.iconUrl}
|
||||||
|
alt={product.name}
|
||||||
|
className="rounded-xl shadow-md w-full h-auto object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Thông tin sản phẩm */}
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-800">{product.name}</h1>
|
||||||
|
<p className="text-2xl font-semibold text-blue-600">
|
||||||
|
{product.price.toLocaleString()} ₫
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={`inline-block px-3 py-1 text-sm font-medium rounded-full ${product.isActive
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-red-100 text-red-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{product.isActive ? 'Đang bán' : 'Ngừng kinh doanh'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-700 mb-2">Mô tả sản phẩm</h2>
|
||||||
|
<div
|
||||||
|
className="prose max-w-none text-gray-600"
|
||||||
|
dangerouslySetInnerHTML={{ __html: product.des }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||||
import type { User } from "../../types/user.type";
|
import type { User } from "../../types/user.type";
|
||||||
import { Apis } from "../../apis";
|
import { Apis } from "../../apis";
|
||||||
|
import type { CartItem } from "../../types/cart.type";
|
||||||
|
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
data: User | null,
|
data: User | null,
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
cart: CartItem[],
|
||||||
|
reloadCart: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const InitUserState: UserState = {
|
const InitUserState: UserState = {
|
||||||
data: null,
|
data: null,
|
||||||
loading: false
|
loading: false,
|
||||||
|
cart: [],
|
||||||
|
reloadCart: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +23,29 @@ const userSlice = createSlice({
|
|||||||
name: "user",
|
name: "user",
|
||||||
initialState: InitUserState,
|
initialState: InitUserState,
|
||||||
reducers: {
|
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) => {
|
extraReducers: (bd) => {
|
||||||
bd.addCase(fetchUserData.pending, (state, action) => {
|
bd.addCase(fetchUserData.pending, (state, action) => {
|
||||||
|
|||||||
6
src/types/cart.type.ts
Normal file
6
src/types/cart.type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface CartItem {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
productId: string;
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user