online and offline base cart
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import * as api from "../service/Service";
|
||||||
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const useGetCart = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["cart"],
|
||||||
|
queryFn: api.getCart,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddToCart = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.addToCart,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateCart = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.updateCart,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveFromCart = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.removeFromCart,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
|
'use client'
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import CartItem from "@/app/cart/components/CartItem"
|
import CartItem from "@/app/cart/components/CartItem"
|
||||||
import CartSummary from "@/components/CartSummary"
|
import CartSummary from "@/components/CartSummary"
|
||||||
import Layout from "@/hoc/Layout"
|
import Layout from "@/hoc/Layout"
|
||||||
import { Trash } from "iconsax-react"
|
import { Trash } from "iconsax-react"
|
||||||
import { NextPage } from "next"
|
import { NextPage } from "next"
|
||||||
|
import { useGetCart } from "./hooks/useCartData"
|
||||||
|
|
||||||
const Cart: NextPage = () => {
|
const Cart: NextPage = () => {
|
||||||
|
|
||||||
|
const { data } = useGetCart();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
|
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
|
||||||
<div className="border-b border-border flex flex-row justify-between items-center gap-3 sm:gap-4">
|
<div className="border-b border-border flex flex-row justify-between items-center gap-3 sm:gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import {
|
||||||
|
AddCartType,
|
||||||
|
UpdateCartType,
|
||||||
|
RemoveCartType,
|
||||||
|
CartResponseType,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const getCart = async (): Promise<CartResponseType> => {
|
||||||
|
const { data } = await axios.get("/cart");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addToCart = async (params: AddCartType) => {
|
||||||
|
const { data } = await axios.post("/cart/add", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCart = async (params: UpdateCartType) => {
|
||||||
|
const { data } = await axios.post("/cart/update", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFromCart = async (params: RemoveCartType) => {
|
||||||
|
const { data } = await axios.post("/cart/remove", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
// Cart Item Types
|
||||||
|
export type CartItemType = {
|
||||||
|
product: {
|
||||||
|
_id: number;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
seoTitle: string | null;
|
||||||
|
seoDescription: string | null;
|
||||||
|
source: string;
|
||||||
|
description: string;
|
||||||
|
metaDescription: string;
|
||||||
|
tags: string[];
|
||||||
|
advantages: string[];
|
||||||
|
disAdvantages: string[];
|
||||||
|
imagesUrl: {
|
||||||
|
cover: string;
|
||||||
|
list: string[];
|
||||||
|
};
|
||||||
|
isFake: string;
|
||||||
|
voice: string | null;
|
||||||
|
specifications: Array<{
|
||||||
|
title: string;
|
||||||
|
values: string[];
|
||||||
|
}>;
|
||||||
|
category: {
|
||||||
|
_id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variant: {
|
||||||
|
_id: string;
|
||||||
|
market_status: string;
|
||||||
|
price: {
|
||||||
|
order_limit: number;
|
||||||
|
retailPrice: number;
|
||||||
|
selling_price: number;
|
||||||
|
is_specialSale: boolean;
|
||||||
|
discount_percent: number;
|
||||||
|
specialSale_order_limit: number | null;
|
||||||
|
specialSale_quantity: number | null;
|
||||||
|
specialSale_endDate: string | null;
|
||||||
|
};
|
||||||
|
stock: number;
|
||||||
|
postingTime: number;
|
||||||
|
isFreeShip: boolean;
|
||||||
|
isWholeSale: boolean;
|
||||||
|
shop: {
|
||||||
|
_id: string;
|
||||||
|
shopName: string;
|
||||||
|
shopCode: number;
|
||||||
|
owner: string;
|
||||||
|
shopDescription: string;
|
||||||
|
telephoneNumber: string;
|
||||||
|
isChatActive: boolean;
|
||||||
|
shopPostalCode: string;
|
||||||
|
logo: string;
|
||||||
|
};
|
||||||
|
shipmentMethod: Array<{
|
||||||
|
_id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
deliveryTime: number;
|
||||||
|
deliveryType: string;
|
||||||
|
}>;
|
||||||
|
warranty: {
|
||||||
|
_id: number;
|
||||||
|
duration: string;
|
||||||
|
logoUrl: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
meterage?: {
|
||||||
|
_id: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
size?: {
|
||||||
|
_id: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cart Types
|
||||||
|
export type CartType = {
|
||||||
|
_id: string;
|
||||||
|
coupon_discount: number;
|
||||||
|
items: CartItemType[];
|
||||||
|
items_count: number;
|
||||||
|
payable_price: number;
|
||||||
|
retail_price: number;
|
||||||
|
total_discount: number;
|
||||||
|
user: {
|
||||||
|
_id: string;
|
||||||
|
fullName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
dateOfBirth: string | null;
|
||||||
|
address: {
|
||||||
|
_id: string;
|
||||||
|
};
|
||||||
|
nationalCode: string | null;
|
||||||
|
homeNumber: string | null;
|
||||||
|
};
|
||||||
|
isWholeSale: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recommended Product Type
|
||||||
|
export type RecommendedProductType = {
|
||||||
|
_id: number;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
seoTitle: string | null;
|
||||||
|
seoDescription: string | null;
|
||||||
|
model: string;
|
||||||
|
source: string;
|
||||||
|
description: string;
|
||||||
|
metaDescription: string;
|
||||||
|
tags: string[];
|
||||||
|
advantages: string[];
|
||||||
|
disAdvantages: string[];
|
||||||
|
totalRate: number;
|
||||||
|
category: string;
|
||||||
|
shop: string;
|
||||||
|
brand: string;
|
||||||
|
status: string;
|
||||||
|
adminComments: string | null;
|
||||||
|
step: number;
|
||||||
|
isFake: boolean;
|
||||||
|
variants: string[];
|
||||||
|
voice: string | null;
|
||||||
|
deleted: boolean;
|
||||||
|
specifications: Array<{
|
||||||
|
title: string;
|
||||||
|
values: string[];
|
||||||
|
}>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
imagesUrl: {
|
||||||
|
cover: string;
|
||||||
|
list: string[];
|
||||||
|
};
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cart API Response Type
|
||||||
|
export type CartResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
cart: CartType;
|
||||||
|
recommended: RecommendedProductType[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cart Action Types
|
||||||
|
export type AddCartType = {
|
||||||
|
productId: number;
|
||||||
|
variantId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateCartType = {
|
||||||
|
productId: number;
|
||||||
|
variantId: string;
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveCartType = {
|
||||||
|
productId: number;
|
||||||
|
variantId: string;
|
||||||
|
};
|
||||||
@@ -1,27 +1,32 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Star1 } from 'iconsax-react'
|
import { Star1 } from 'iconsax-react'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import { Product } from '@/types/product.types'
|
import { Product } from '@/types/product.types'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
|
import { useProductStore } from '../store/Store'
|
||||||
|
|
||||||
interface BaseInformationProps {
|
interface BaseInformationProps {
|
||||||
product: Product
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
|
const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
|
||||||
|
|
||||||
|
const { setVariantId, variantId } = useProductStore()
|
||||||
|
|
||||||
const brand = product.brand
|
const brand = product.brand
|
||||||
|
|
||||||
// State برای انتخاب variant ها
|
// تابع برای انتخاب variant
|
||||||
const [selectedSize, setSelectedSize] = useState<string | null>(
|
const handleVariantSelect = (variantId: string) => {
|
||||||
product.variants.find(v => v.size)?.size?._id?.toString() || null
|
setVariantId(variantId)
|
||||||
)
|
}
|
||||||
const [selectedColor, setSelectedColor] = useState<string | null>(
|
|
||||||
product.variants.find(v => v.color)?.color?._id?.toString() || null
|
// انتخاب اولین variant به صورت پیشفرض
|
||||||
)
|
useEffect(() => {
|
||||||
const [selectedMeterage, setSelectedMeterage] = useState<string | null>(
|
if (product.variants.length > 0 && !variantId) {
|
||||||
product.variants.find(v => v.meterage)?.meterage?._id?.toString() || null
|
setVariantId(product.variants[0]._id)
|
||||||
)
|
}
|
||||||
|
}, [product.variants, variantId, setVariantId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
@@ -54,11 +59,11 @@ const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
|
|||||||
<div className='flex flex-wrap items-center gap-1.5 mb-4'>
|
<div className='flex flex-wrap items-center gap-1.5 mb-4'>
|
||||||
{product.variants.map((variant) => {
|
{product.variants.map((variant) => {
|
||||||
if (variant.meterage) {
|
if (variant.meterage) {
|
||||||
const isSelected = selectedMeterage === variant.meterage._id.toString()
|
const isSelected = variantId === variant._id
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={variant._id}
|
key={variant._id}
|
||||||
onClick={() => setSelectedMeterage(variant.meterage!._id.toString())}
|
onClick={() => handleVariantSelect(variant._id)}
|
||||||
className={`text-[13px] px-4 py-2 border border-border rounded-lg whitespace-nowrap transition-colors ${isSelected
|
className={`text-[13px] px-4 py-2 border border-border rounded-lg whitespace-nowrap transition-colors ${isSelected
|
||||||
? 'bg-primary text-white border-primary'
|
? 'bg-primary text-white border-primary'
|
||||||
: 'hover:border-primary/50'
|
: 'hover:border-primary/50'
|
||||||
@@ -76,11 +81,11 @@ const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
|
|||||||
<div className='flex flex-wrap items-center gap-1.5 mb-4'>
|
<div className='flex flex-wrap items-center gap-1.5 mb-4'>
|
||||||
{product.variants.map((variant) => {
|
{product.variants.map((variant) => {
|
||||||
if (variant.color) {
|
if (variant.color) {
|
||||||
const isSelected = selectedColor === variant.color._id.toString()
|
const isSelected = variantId === variant._id
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={variant._id}
|
key={variant._id}
|
||||||
onClick={() => setSelectedColor(variant.color!._id.toString())}
|
onClick={() => handleVariantSelect(variant._id)}
|
||||||
className={clx(
|
className={clx(
|
||||||
'size-10 rounded-full flex justify-center items-center',
|
'size-10 rounded-full flex justify-center items-center',
|
||||||
variant.color.hexColor === '#FFFFFF' ? 'border border-border' : ''
|
variant.color.hexColor === '#FFFFFF' ? 'border border-border' : ''
|
||||||
@@ -102,11 +107,11 @@ const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
|
|||||||
<div className='flex flex-wrap items-center gap-1.5'>
|
<div className='flex flex-wrap items-center gap-1.5'>
|
||||||
{product.variants.map((variant) => {
|
{product.variants.map((variant) => {
|
||||||
if (variant.size) {
|
if (variant.size) {
|
||||||
const isSelected = selectedSize === variant.size._id.toString()
|
const isSelected = variantId === variant._id
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={variant._id}
|
key={variant._id}
|
||||||
onClick={() => setSelectedSize(variant.size!._id.toString())}
|
onClick={() => handleVariantSelect(variant._id)}
|
||||||
className={`text-[13px] px-4 py-2 border border-border rounded-lg whitespace-nowrap transition-colors ${isSelected
|
className={`text-[13px] px-4 py-2 border border-border rounded-lg whitespace-nowrap transition-colors ${isSelected
|
||||||
? 'bg-primary text-white border-primary'
|
? 'bg-primary text-white border-primary'
|
||||||
: 'hover:border-primary/50'
|
: 'hover:border-primary/50'
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useProductStore } from '../store/Store'
|
||||||
|
import { useSharedStore } from '@/share/store/sharedStore'
|
||||||
|
import OnlineCart from './OnlineCart'
|
||||||
|
import OfflineCart from './OfflineCart'
|
||||||
|
|
||||||
|
const Cart = () => {
|
||||||
|
const { variantId } = useProductStore()
|
||||||
|
const { isLogin } = useSharedStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLogin ? (
|
||||||
|
<OnlineCart variantId={variantId} />
|
||||||
|
) : (
|
||||||
|
<OfflineCart variantId={variantId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Cart
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { Add, Minus, Trash } from 'iconsax-react'
|
||||||
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { useLocalCart } from '../hooks/useLocalCart'
|
||||||
|
|
||||||
|
interface OfflineCartProps {
|
||||||
|
variantId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const OfflineCart: React.FC<OfflineCartProps> = ({ variantId }) => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
// Local cart hooks for non-logged in users
|
||||||
|
const {
|
||||||
|
addToLocalCart,
|
||||||
|
updateLocalCartItem,
|
||||||
|
removeFromLocalCart,
|
||||||
|
getLocalCartItem,
|
||||||
|
} = useLocalCart()
|
||||||
|
|
||||||
|
const [isInCart, setIsInCart] = useState<boolean>(false)
|
||||||
|
const [quantity, setQuantity] = useState<number>(1)
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
addToLocalCart(Number(id), variantId, 1)
|
||||||
|
setIsInCart(true)
|
||||||
|
setQuantity(1)
|
||||||
|
toast('محصول به سبد خرید اضافه شد', 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check local cart for non-logged in users
|
||||||
|
const localItem = getLocalCartItem(Number(id), variantId)
|
||||||
|
if (localItem) {
|
||||||
|
setIsInCart(true)
|
||||||
|
setQuantity(localItem.quantity)
|
||||||
|
} else {
|
||||||
|
setIsInCart(false)
|
||||||
|
}
|
||||||
|
}, [variantId, id, getLocalCartItem])
|
||||||
|
|
||||||
|
const handleQuantityChange = (number: number) => {
|
||||||
|
if (number === 0) {
|
||||||
|
handleEmptyCart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuantity(number)
|
||||||
|
updateLocalCartItem(Number(id), variantId, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmptyCart = () => {
|
||||||
|
removeFromLocalCart(Number(id), variantId)
|
||||||
|
toast('محصول از سبد خرید حذف شد', 'success')
|
||||||
|
setIsInCart(false)
|
||||||
|
setQuantity(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!isInCart ?
|
||||||
|
<Button className='w-full mt-8' onClick={handleAddToCart}>
|
||||||
|
افزودن به سبد خرید
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<div className={"flex items-center gap-2 sm:gap-4 mt-8"}>
|
||||||
|
<div className="h-10 sm:h-12 rounded-lg sm:rounded-xl border border-border bg-white px-2 sm:px-4 flex-1 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuantityChange(quantity + 1)}
|
||||||
|
className="p-1"
|
||||||
|
aria-label="increase quantity"
|
||||||
|
>
|
||||||
|
<Add size={18} color={PRIMARY_COLOR} className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm sm:text-[18px] font-medium text-primary">
|
||||||
|
{quantity}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuantityChange(quantity - 1)}
|
||||||
|
className="p-1"
|
||||||
|
aria-label="decrease quantity"
|
||||||
|
>
|
||||||
|
<Minus size={18} color={PRIMARY_COLOR} className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleEmptyCart}
|
||||||
|
className="h-10 w-10 sm:h-12 sm:w-12 rounded-lg sm:rounded-xl border border-border grid place-items-center bg-white"
|
||||||
|
aria-label="remove from cart"
|
||||||
|
>
|
||||||
|
<Trash size={18} color="#AD3434" className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OfflineCart
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useAddToCart, useGetCart, useRemoveFromCart, useUpdateCart } from '@/app/cart/hooks/useCartData'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useParams } from 'next/navigation'
|
||||||
|
import { Add, Minus, Trash } from 'iconsax-react'
|
||||||
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { extractErrorMessage } from '@/helpers/errorUtils'
|
||||||
|
|
||||||
|
interface OnlineCartProps {
|
||||||
|
variantId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnlineCart: React.FC<OnlineCartProps> = ({ variantId }) => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
// API hooks for logged in users
|
||||||
|
const { mutate: addToCart, isPending: isAddingToCart } = useAddToCart()
|
||||||
|
const { mutate: updateCart, isPending: isUpdatingCart } = useUpdateCart()
|
||||||
|
const { mutate: removeFromCart, isPending: isRemovingFromCart } = useRemoveFromCart()
|
||||||
|
const { data: cart, isPending } = useGetCart()
|
||||||
|
|
||||||
|
const [isInCart, setIsInCart] = useState<boolean>(false)
|
||||||
|
const [quantity, setQuantity] = useState<number>(1)
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
addToCart({
|
||||||
|
productId: Number(id),
|
||||||
|
variantId: variantId,
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsInCart(true)
|
||||||
|
setQuantity(1)
|
||||||
|
toast('محصول به سبد خرید اضافه شد', 'success')
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check API cart for logged in users
|
||||||
|
const item = cart?.results?.cart?.items?.find(item => item.variant?._id === variantId)
|
||||||
|
if (item) {
|
||||||
|
setIsInCart(true)
|
||||||
|
setQuantity(item.quantity)
|
||||||
|
} else {
|
||||||
|
setIsInCart(false)
|
||||||
|
}
|
||||||
|
}, [cart, variantId])
|
||||||
|
|
||||||
|
const handleQuantityChange = (number: number) => {
|
||||||
|
if (number === 0) {
|
||||||
|
handleEmptyCart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldQuantity = quantity
|
||||||
|
setQuantity(number)
|
||||||
|
|
||||||
|
updateCart({
|
||||||
|
productId: Number(id),
|
||||||
|
variantId: variantId,
|
||||||
|
quantity: number,
|
||||||
|
}, {
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
setQuantity(oldQuantity)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmptyCart = () => {
|
||||||
|
removeFromCart({
|
||||||
|
productId: Number(id),
|
||||||
|
variantId: variantId,
|
||||||
|
}, {
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast('محصول از سبد خرید حذف شد', 'success')
|
||||||
|
setIsInCart(false)
|
||||||
|
setQuantity(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!isInCart && !isPending ?
|
||||||
|
<Button isLoading={isAddingToCart} className='w-full mt-8' onClick={handleAddToCart}>
|
||||||
|
افزودن به سبد خرید
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
<div className={"flex items-center gap-2 sm:gap-4 mt-8"}>
|
||||||
|
<div className="h-10 sm:h-12 rounded-lg sm:rounded-xl border border-border bg-white px-2 sm:px-4 flex-1 flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuantityChange(quantity + 1)}
|
||||||
|
className="disabled:opacity-40 disabled:cursor-not-allowed p-1"
|
||||||
|
aria-label="increase quantity"
|
||||||
|
disabled={isAddingToCart || isUpdatingCart}
|
||||||
|
>
|
||||||
|
<Add size={18} color={PRIMARY_COLOR} className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm sm:text-[18px] font-medium text-primary">
|
||||||
|
{quantity}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleQuantityChange(quantity - 1)}
|
||||||
|
className="disabled:opacity-40 disabled:cursor-not-allowed p-1"
|
||||||
|
disabled={isAddingToCart || isUpdatingCart}
|
||||||
|
aria-label="decrease quantity"
|
||||||
|
>
|
||||||
|
<Minus size={18} color={PRIMARY_COLOR} className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleEmptyCart}
|
||||||
|
className="h-10 w-10 sm:h-12 sm:w-12 rounded-lg sm:rounded-xl border border-border grid place-items-center bg-white"
|
||||||
|
aria-label="remove from cart"
|
||||||
|
disabled={isRemovingFromCart}
|
||||||
|
>
|
||||||
|
<Trash size={18} color="#AD3434" className="sm:w-5 sm:h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnlineCart
|
||||||
@@ -5,12 +5,14 @@ import { BoxTick, I3DRotate } from 'iconsax-react'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Product } from '@/types/product.types'
|
import { Product } from '@/types/product.types'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import Cart from './Cart'
|
||||||
|
|
||||||
interface ShopInformationProps {
|
interface ShopInformationProps {
|
||||||
product: Product
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShopInformation = ({ product }: ShopInformationProps) => {
|
const ShopInformation = ({ product }: ShopInformationProps) => {
|
||||||
|
|
||||||
const defaultVariant = product.default_variant
|
const defaultVariant = product.default_variant
|
||||||
const shop = defaultVariant.shop
|
const shop = defaultVariant.shop
|
||||||
const price = defaultVariant.price
|
const price = defaultVariant.price
|
||||||
@@ -94,9 +96,7 @@ const ShopInformation = ({ product }: ShopInformationProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className='w-full mt-8'>
|
<Cart />
|
||||||
افزودن به سبد خرید
|
|
||||||
</Button>
|
|
||||||
<Button className='w-full mt-3 bg-[#323232]'>
|
<Button className='w-full mt-3 bg-[#323232]'>
|
||||||
گفتگو با فروشنده
|
گفتگو با فروشنده
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useLocalCartStore, LocalCartItem } from "../store/LocalCartStore";
|
||||||
|
|
||||||
|
export const useLocalCart = () => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
|
getItem,
|
||||||
|
clearCart,
|
||||||
|
} = useLocalCartStore();
|
||||||
|
|
||||||
|
const addToLocalCart = (
|
||||||
|
productId: number,
|
||||||
|
variantId: string,
|
||||||
|
quantity: number = 1
|
||||||
|
) => {
|
||||||
|
addItem({ productId, variantId, quantity });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLocalCartItem = (
|
||||||
|
productId: number,
|
||||||
|
variantId: string,
|
||||||
|
quantity: number
|
||||||
|
) => {
|
||||||
|
if (quantity <= 0) {
|
||||||
|
removeFromLocalCart(productId, variantId);
|
||||||
|
} else {
|
||||||
|
updateItem(productId, variantId, quantity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromLocalCart = (productId: number, variantId: string) => {
|
||||||
|
removeItem(productId, variantId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalCartItem = (productId: number, variantId: string) => {
|
||||||
|
return getItem(productId, variantId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalCartItems = () => {
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalCartItemsCount = () => {
|
||||||
|
return items.reduce((total, item) => total + item.quantity, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLocalCart = () => {
|
||||||
|
clearCart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
localCartItems: items,
|
||||||
|
addToLocalCart,
|
||||||
|
updateLocalCartItem,
|
||||||
|
removeFromLocalCart,
|
||||||
|
getLocalCartItem,
|
||||||
|
getLocalCartItems,
|
||||||
|
getLocalCartItemsCount,
|
||||||
|
clearLocalCart,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type LocalCartItem = {
|
||||||
|
productId: number;
|
||||||
|
variantId: string;
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalCartStoreType = {
|
||||||
|
items: LocalCartItem[];
|
||||||
|
addItem: (item: LocalCartItem) => void;
|
||||||
|
updateItem: (productId: number, variantId: string, quantity: number) => void;
|
||||||
|
removeItem: (productId: number, variantId: string) => void;
|
||||||
|
getItem: (productId: number, variantId: string) => LocalCartItem | undefined;
|
||||||
|
clearCart: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocalCartStore = create<LocalCartStoreType>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
addItem: (item: LocalCartItem) => {
|
||||||
|
const existingItem = get().items.find(
|
||||||
|
(i) =>
|
||||||
|
i.productId === item.productId && i.variantId === item.variantId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
set((state) => ({
|
||||||
|
items: state.items.map((i) =>
|
||||||
|
i.productId === item.productId && i.variantId === item.variantId
|
||||||
|
? { ...i, quantity: i.quantity + item.quantity }
|
||||||
|
: i
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set((state) => ({
|
||||||
|
items: [...state.items, item],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateItem: (productId: number, variantId: string, quantity: number) => {
|
||||||
|
set((state) => ({
|
||||||
|
items: state.items.map((item) =>
|
||||||
|
item.productId === productId && item.variantId === variantId
|
||||||
|
? { ...item, quantity }
|
||||||
|
: item
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem: (productId: number, variantId: string) => {
|
||||||
|
set((state) => ({
|
||||||
|
items: state.items.filter(
|
||||||
|
(item) =>
|
||||||
|
!(item.productId === productId && item.variantId === variantId)
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getItem: (productId: number, variantId: string) => {
|
||||||
|
return get().items.find(
|
||||||
|
(item) => item.productId === productId && item.variantId === variantId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCart: () => {
|
||||||
|
set({ items: [] });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "local-cart-storage",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { ProductStoreType } from "../types/Types";
|
||||||
|
|
||||||
|
export const useProductStore = create<ProductStoreType>((set) => ({
|
||||||
|
variantId: "",
|
||||||
|
setVariantId: (value) => set({ variantId: value }),
|
||||||
|
}));
|
||||||
@@ -11,3 +11,8 @@ export type AddQuestionProps = {
|
|||||||
content: string;
|
content: string;
|
||||||
productId?: string;
|
productId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductStoreType = {
|
||||||
|
variantId: string;
|
||||||
|
setVariantId: (value: string) => void;
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const ToastContainer: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-[9999999999] flex flex-col gap-2">
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
<div
|
<div
|
||||||
key={toast.id}
|
key={toast.id}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
export const TOKEN_NAME = "sh_token";
|
export const TOKEN_NAME = "sh_token";
|
||||||
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
||||||
// export const BASE_URL = "https://api.shinan.ir";
|
export const BASE_URL = "https://api.shinan.ir";
|
||||||
export const BASE_URL = "https://api-sondos.run.danakcorp.com";
|
// export const BASE_URL = "https://api-sondos.run.danakcorp.com";
|
||||||
|
|
||||||
export const PRIMARY_COLOR = "#015699";
|
export const PRIMARY_COLOR = "#015699";
|
||||||
|
|||||||
+15
-1
@@ -2,11 +2,25 @@
|
|||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { DocumentText, Home, Profile, ShoppingCart, HambergerMenu } from 'iconsax-react'
|
import { DocumentText, Home, Profile, ShoppingCart, HambergerMenu } from 'iconsax-react'
|
||||||
import { FC, Fragment, useState } from 'react'
|
import { FC, Fragment, useEffect, useState } from 'react'
|
||||||
import Menu from './components/Menu'
|
import Menu from './components/Menu'
|
||||||
|
import { useSharedStore } from './store/sharedStore'
|
||||||
|
import { getToken } from '@/config/func'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
|
const { setIsLogin } = useSharedStore()
|
||||||
|
|
||||||
|
const handleSetIsLogin = async () => {
|
||||||
|
const token = await getToken()
|
||||||
|
setIsLogin(token ? true : false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSetIsLogin()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|||||||
Reference in New Issue
Block a user