base logig pages cart shipping and payment

This commit is contained in:
hamid zarghami
2025-09-14 12:47:51 +03:30
parent 7b05f96576
commit 6d52d1e91a
13 changed files with 663 additions and 82 deletions
+57 -16
View File
@@ -5,14 +5,49 @@ import { clx } from '@/helpers/utils';
import { ArchiveTick, Shop, TruckFast } from 'iconsax-react'
import Image from 'next/image'
import { FC } from 'react'
import { CartItemType } from '../types/Types'
import { useUpdateCart, useRemoveFromCart } from '../hooks/useCartData'
import { toast } from '@/components/Toast'
type Props = {
noBorder?: boolean
item: CartItemType
}
const CartItem: FC<Props> = (props) => {
const { noBorder } = props
const { noBorder, item } = props
const updateCartMutation = useUpdateCart()
const removeFromCartMutation = useRemoveFromCart()
if (!item) {
return null
}
const handleQuantityChange = async (newQuantity: number) => {
try {
await updateCartMutation.mutateAsync({
productId: item.product._id,
variantId: item.variant._id,
quantity: newQuantity
})
toast('تعداد به‌روزرسانی شد', 'success')
} catch {
toast('خطا در به‌روزرسانی تعداد', 'error')
}
}
const handleRemove = async () => {
try {
await removeFromCartMutation.mutateAsync({
productId: item.product._id,
variantId: item.variant._id
})
toast('محصول از سبد خرید حذف شد', 'success')
} catch {
toast('خطا در حذف محصول', 'error')
}
}
return (
<div className={clx(
@@ -21,8 +56,8 @@ const CartItem: FC<Props> = (props) => {
)}>
<div className="flex-shrink-0 flex justify-center sm:justify-start">
<Image
src={'https://dkstatics-public.digikala.com/digikala-products/40026b18c2b053ac4a68c3288556dc899a77aecd_1727277566.jpg?x-oss-process=image/resize,m_lfit,h_300,w_300/format,webp/quality,q_80'}
alt=''
src={item.product.imagesUrl.cover}
alt={item.product.title_fa}
width={200}
height={200}
className='w-24 h-24 sm:w-32 sm:h-32 lg:max-w-[150px] lg:max-h-[150px] object-contain'
@@ -32,33 +67,35 @@ const CartItem: FC<Props> = (props) => {
<div className='flex-1 min-w-0'>
<div className='flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2 sm:gap-4'>
<div className='text-sm sm:text-base font-medium leading-relaxed sm:max-w-[250px] sm:truncate'>
گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 1 ترابایت و رم 12 گیگابایت - ویتنام
{item.product.title_fa}
</div>
<div className="text-lg sm:text-xl font-semibold text-primary">
70,400,000 <span className='text-xs sm:text-sm'>تومان</span>
{item.variant.price.selling_price.toLocaleString()} <span className='text-xs sm:text-sm'>تومان</span>
</div>
</div>
<div className='font-light text-[#333333] mt-4 sm:mt-6 lg:mt-14'>
<div className='flex items-center gap-2.5 mb-3 sm:mb-4'>
<div className='w-5 sm:w-6 flex-shrink-0'>
<div className='size-3 sm:size-[14px] rounded-full bg-black'></div>
{item.variant.size && (
<div className='flex items-center gap-2.5 mb-3 sm:mb-4'>
<div className='w-5 sm:w-6 flex-shrink-0'>
<div className='size-3 sm:size-[14px] rounded-full bg-black'></div>
</div>
<div className='text-xs sm:text-sm'>{item.variant.size.value}</div>
</div>
<div className='text-xs sm:text-sm'>مشکی</div>
</div>
)}
<div className='flex items-center gap-2.5 mb-3 sm:mb-4'>
<div className='w-5 sm:w-6 flex-shrink-0'>
<Shop size={16} color='#333333' className="sm:w-5 sm:h-5" />
</div>
<div className='text-xs sm:text-sm'>فروشنده فروشگاه سندس</div>
<div className='text-xs sm:text-sm'>فروشنده {item.variant.shop.shopName}</div>
</div>
<div className='flex items-center gap-2.5 mb-4 sm:mb-6'>
<div className='w-5 sm:w-6 flex-shrink-0'>
<ArchiveTick size={16} color='#333333' className="sm:w-5 sm:h-5" />
</div>
<div className='text-xs sm:text-sm'>گارانتی اصالت و سلامت فیزیکی کالا</div>
<div className='text-xs sm:text-sm'>گارانتی {item.variant.warranty.name}</div>
</div>
<div className='flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4'>
@@ -66,14 +103,18 @@ const CartItem: FC<Props> = (props) => {
<div className='w-5 sm:w-6 flex-shrink-0'>
<TruckFast size={16} color='blue' className="sm:w-5 sm:h-5" />
</div>
<div className='text-xs sm:text-sm'>ارسال از سندس حداکثر پس از 3 روز کاری</div>
<div className='text-xs sm:text-sm'>
ارسال از {item.variant.shop.shopName} حداکثر پس از {item.variant.shipmentMethod[0]?.deliveryTime || 3} روز کاری
</div>
</div>
<div className="flex justify-center sm:justify-end">
<CartControls
onChange={() => null}
onRemove={() => null}
quantity={1}
onChange={handleQuantityChange}
onRemove={handleRemove}
quantity={item.quantity}
max={item.variant.stock}
className={updateCartMutation.isPending || removeFromCartMutation.isPending ? 'opacity-50 pointer-events-none' : ''}
/>
</div>
</div>
+71 -9
View File
@@ -1,16 +1,78 @@
import { Button } from '@/components/ui/button'
import { FC } from 'react'
import Input from '@/components/Input'
import { FC, useState } from 'react'
const DiscountCard: FC = () => {
return (
<div className='h-16 border border-border rounded-2xl flex justify-between mt-5 items-center px-6'>
<div>
کد تخفیف دارید؟
type DiscountCardProps = {
discountCode: string
setDiscountCode: (code: string) => void
onSubmit: () => void
isLoading?: boolean
}
const DiscountCard: FC<DiscountCardProps> = ({
discountCode,
setDiscountCode,
onSubmit,
isLoading = false
}) => {
const [isExpanded, setIsExpanded] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit()
}
if (!isExpanded) {
return (
<div className='h-16 border border-border rounded-2xl flex justify-between mt-5 items-center px-6'>
<div>
کد تخفیف دارید؟
</div>
<Button
variant={'ghost'}
className='text-left p-0 text-primary hover:bg-transparent cursor-pointer'
onClick={() => setIsExpanded(true)}
>
ثبت کد
</Button>
</div>
)
}
<Button variant={'ghost'} className='text-left p-0 text-primary hover:bg-transparent cursor-pointer'>
ثبت کد
</Button>
return (
<div className='border border-border rounded-2xl mt-5 p-4 sm:p-6'>
<div className='text-sm font-medium mb-4'>کد تخفیف</div>
<form onSubmit={handleSubmit} className='flex gap-3'>
<Input
type="text"
placeholder="کد تخفیف خود را وارد کنید"
value={discountCode}
onChange={(e) => setDiscountCode(e.target.value)}
className="flex-1"
disabled={isLoading}
label=""
/>
<Button
type="submit"
disabled={isLoading || !discountCode.trim()}
className="px-6"
>
{isLoading ? 'در حال اعمال...' : 'اعمال'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
setIsExpanded(false)
setDiscountCode('')
}}
className="px-4"
>
لغو
</Button>
</form>
</div>
)
}
+30 -1
View File
@@ -1,5 +1,5 @@
import * as api from "../service/Service";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export const useGetCart = () => {
return useQuery({
@@ -9,19 +9,48 @@ export const useGetCart = () => {
};
export const useAddToCart = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.addToCart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
};
export const useUpdateCart = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.updateCart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
};
export const useRemoveFromCart = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.removeFromCart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
};
export const useShippingCost = () => {
return useQuery({
queryKey: ["shipping-cost"],
queryFn: api.shippingCost,
});
};
export const usePaymentMethods = () => {
return useQuery({
queryKey: ["payment-methods"],
queryFn: api.paymentMethods,
});
};
+89 -12
View File
@@ -5,11 +5,72 @@ import CartSummary from "@/components/CartSummary"
import Layout from "@/hoc/Layout"
import { Trash } from "iconsax-react"
import { NextPage } from "next"
import { useGetCart } from "./hooks/useCartData"
import { useGetCart, useRemoveFromCart } from "./hooks/useCartData"
import { toast } from "@/components/Toast"
import { useState } from "react"
const Cart: NextPage = () => {
const { data } = useGetCart();
const { data, isLoading, error } = useGetCart();
const removeFromCartMutation = useRemoveFromCart()
const [isClearingAll, setIsClearingAll] = useState(false)
const handleClearAll = async () => {
if (!data?.results?.cart?.items?.length) return
setIsClearingAll(true)
try {
// حذف همه آیتم‌ها یکی یکی
const removePromises = data.results.cart.items
.filter(item => item && item.product && item.variant)
.map(item =>
removeFromCartMutation.mutateAsync({
productId: item.product._id,
variantId: item.variant._id
})
)
await Promise.all(removePromises)
toast('همه محصولات از سبد خرید حذف شدند', 'success')
} catch {
toast('خطا در حذف محصولات', 'error')
} finally {
setIsClearingAll(false)
}
}
if (isLoading) {
return (
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">در حال بارگذاری...</div>
</div>
</div>
)
}
if (error) {
return (
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-red-500">خطا در بارگذاری سبد خرید</div>
</div>
</div>
)
}
const cart = data?.results?.cart
const items = cart?.items || []
if (!items.length) {
return (
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">سبد خرید شما خالی است</div>
</div>
</div>
)
}
return (
<div className="mt-12 sm:mt-14 px-3 sm:px-4 md:px-8 lg:px-20">
@@ -19,31 +80,47 @@ const Cart: NextPage = () => {
سبد خرید
</div>
<div className="size-6 sm:size-7 lg:size-8 rounded-md sm:rounded-lg bg-[#F2F2F2] flex items-center justify-center text-primary text-xs sm:text-sm font-medium">
10
{cart?.items_count || 0}
</div>
</div>
<Button
className="bg-white shadow h-8 sm:h-9 lg:h-10 w-auto px-2 sm:px-3 lg:px-4 flex-shrink-0"
onClick={handleClearAll}
disabled={isClearingAll || !items.length}
className="bg-white shadow h-8 sm:h-9 lg:h-10 w-auto px-2 sm:px-3 lg:px-4 flex-shrink-0 disabled:opacity-50"
>
<div className="flex items-center gap-1 sm:gap-1.5 lg:gap-2">
<Trash size={12} color="#AD3434" className="sm:w-4 sm:h-4 lg:w-5 lg:h-5" />
<div className="text-xs sm:text-sm text-[#AD3434] font-light hidden sm:block">حذف همه از سبد خرید</div>
<div className="text-xs text-[#AD3434] font-light sm:hidden">حذف همه</div>
<div className="text-xs sm:text-sm text-[#AD3434] font-light hidden sm:block">
{isClearingAll ? 'در حال حذف...' : 'حذف همه از سبد خرید'}
</div>
<div className="text-xs text-[#AD3434] font-light sm:hidden">
{isClearingAll ? 'حذف...' : 'حذف همه'}
</div>
</div>
</Button>
</div>
<div className="mt-4 sm:mt-5 lg:mt-6 flex flex-col lg:flex-row gap-3 sm:gap-4 lg:gap-6">
<div className="flex-1 border border-border rounded-lg sm:rounded-xl lg:rounded-2xl overflow-hidden">
<CartItem noBorder />
<CartItem />
{items.map((item, index) => {
if (!item || !item.product || !item.variant) {
return null
}
return (
<CartItem
key={`${item.product._id}-${item.variant._id}`}
item={item}
noBorder={index === 0}
/>
)
})}
</div>
<CartSummary
itemsCount={3}
itemsPrice={580000}
discount={580000}
total={80580000}
itemsCount={cart?.items_count || 0}
itemsPrice={cart?.retail_price || 0}
discount={cart?.total_discount || 0}
total={cart?.payable_price || 0}
confirmHref="/cart/shipping"
confirmLabel="ادامه به آدرس و ارسال"
className="w-full lg:w-[360px] xl:w-[400px]"
+128 -15
View File
@@ -1,43 +1,156 @@
'use client'
import CartSummary from '@/components/CartSummary'
import Layout from '@/hoc/Layout'
import { NextPage } from 'next'
import TitleBack from '../components/TitleBack'
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import DiscountCard from '../components/DiscountCard'
import { usePaymentMethods, useGetCart, useShippingCost } from '../hooks/useCartData'
import { useState } from 'react'
import { toast } from '@/components/Toast'
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
const CartPayment: NextPage = () => {
const { data: paymentMethodsData, isLoading: paymentMethodsLoading } = usePaymentMethods()
const { data: cartData, isLoading: cartLoading } = useGetCart()
const { data: shippingData, isLoading: shippingLoading } = useShippingCost()
const { data: profileData, isLoading: profileLoading } = useGetProfile()
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('')
const [discountCode, setDiscountCode] = useState<string>('')
const [isApplyingDiscount, setIsApplyingDiscount] = useState(false)
const isLoading = paymentMethodsLoading || cartLoading || shippingLoading || profileLoading
if (isLoading) {
return (
<div className="mt-14 px-4 sm:px-8 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">در حال بارگذاری...</div>
</div>
</div>
)
}
const cart = cartData?.results?.cart
const paymentMethods = paymentMethodsData?.results?.paymentMethods || []
const profile = profileData?.results?.info
if (!cart?.items?.length) {
return (
<div className="mt-14 px-4 sm:px-8 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">سبد خرید شما خالی است</div>
</div>
</div>
)
}
// محاسبه هزینه ارسال
const totalShippingCost = shippingData?.results?.shipping?.reduce((total, shop) => {
return total + (shop.shippers?.[0]?.totalShippingCost || 0)
}, 0) || 0
// محاسبه قیمت نهایی
const finalTotal = (cart?.payable_price || 0) + totalShippingCost
const handlePaymentMethodChange = (value: string) => {
setSelectedPaymentMethod(value)
}
const handleDiscountCodeSubmit = async () => {
if (!discountCode.trim()) {
toast('لطفاً کد تخفیف را وارد کنید', 'error')
return
}
setIsApplyingDiscount(true)
try {
// TODO: پیاده‌سازی API اعمال کد تخفیف
// await applyDiscountCode(discountCode)
toast('کد تخفیف با موفقیت اعمال شد', 'success')
} catch {
toast('کد تخفیف نامعتبر است', 'error')
} finally {
setIsApplyingDiscount(false)
}
}
const handlePayment = async () => {
if (!selectedPaymentMethod) {
toast('لطفاً روش پرداخت را انتخاب کنید', 'error')
return
}
if (!profile?.userAddress) {
toast('لطفاً ابتدا آدرس خود را تکمیل کنید', 'error')
return
}
try {
// TODO: پیاده‌سازی API پرداخت نهایی
// await processPayment({
// paymentMethodId: selectedPaymentMethod,
// discountCode: discountCode || null,
// shippingAddress: profile.userAddress
// })
toast('پرداخت با موفقیت انجام شد', 'success')
} catch {
toast('خطا در پردازش پرداخت', 'error')
}
}
return (
<div className="mt-14 px-4 sm:px-8 lg:px-20">
<div className="mt-6 flex flex-col lg:flex-row gap-6">
<div className='flex-1'>
<TitleBack title='پرداخت' />
<DiscountCard
discountCode={discountCode}
setDiscountCode={setDiscountCode}
onSubmit={handleDiscountCodeSubmit}
isLoading={isApplyingDiscount}
/>
<div className='mt-5 border border-border rounded-2xl p-4 sm:p-6 font-light'>
<div>انتخاب روش پرداخت</div>
<div className='mt-6 sm:mt-10 text-[#333333] font-light'>
<RadioGroup defaultValue="option-one">
<div className="flex items-center gap-2">
<RadioGroupItem value="option-one" id="option-one" />
<div className='text-sm'>زین پال</div>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option-one" id="option-one" />
<div className='text-sm'>زین پال</div>
</div>
<RadioGroup
value={selectedPaymentMethod}
onValueChange={handlePaymentMethodChange}
>
{paymentMethods.map((method) => (
<div key={method._id} className="flex items-center gap-2 mb-3">
<RadioGroupItem
value={method._id}
id={method._id}
disabled={!method.isActive}
/>
<label
htmlFor={method._id}
className={`text-sm ${method.isActive ? 'cursor-pointer' : 'cursor-not-allowed'} ${!method.isActive ? 'text-gray-400' : ''}`}
>
<span className="font-medium">{method.title_fa}</span>
{method.provider && (
<span className="text-xs text-gray-500 mr-2">
({method.provider})
</span>
)}
</label>
</div>
))}
</RadioGroup>
</div>
</div>
</div>
<div className="w-full lg:w-[360px] lg:flex-shrink-0">
<CartSummary
itemsCount={3}
itemsPrice={580000}
discount={580000}
total={80580000}
confirmHref="/cart/payment"
itemsCount={cart?.items_count || 0}
itemsPrice={cart?.retail_price || 0}
discount={cart?.total_discount || 0}
shippingCost={totalShippingCost}
total={finalTotal}
onConfirm={handlePayment}
confirmLabel="پرداخت"
className="w-full"
/>
+12
View File
@@ -4,6 +4,8 @@ import {
UpdateCartType,
RemoveCartType,
CartResponseType,
ShippingCostResponseType,
PaymentMethodsResponseType,
} from "../types/Types";
export const getCart = async (): Promise<CartResponseType> => {
@@ -25,3 +27,13 @@ export const removeFromCart = async (params: RemoveCartType) => {
const { data } = await axios.post("/cart/remove", params);
return data;
};
export const shippingCost = async (): Promise<ShippingCostResponseType> => {
const { data } = await axios.get("/shipment/shipping-cost");
return data;
};
export const paymentMethods = async (): Promise<PaymentMethodsResponseType> => {
const { data } = await axios.get("/payment/methods");
return data;
};
+77 -11
View File
@@ -1,11 +1,63 @@
'use client'
import CartSummary from '@/components/CartSummary'
import Layout from '@/hoc/Layout'
import { NextPage } from 'next'
import CartItem from '../components/CartItem'
import TitleBack from '../components/TitleBack'
import { Location } from 'iconsax-react'
import { useGetCart, useShippingCost } from '../hooks/useCartData'
import { CartItemType } from '../types/Types'
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
import Link from 'next/link'
const CartShipping: NextPage = () => {
const { data: cartData, isLoading: cartLoading, error: cartError } = useGetCart()
const { data: shippingData, isLoading: shippingLoading, error: shippingError } = useShippingCost()
const { data: profileData, isLoading: profileLoading, error: profileError } = useGetProfile()
if (cartLoading || shippingLoading || profileLoading) {
return (
<div className="mt-14 px-4 sm:px-6 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">در حال بارگذاری...</div>
</div>
</div>
)
}
if (cartError || shippingError || profileError) {
return (
<div className="mt-14 px-4 sm:px-6 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-red-500">خطا در بارگذاری اطلاعات</div>
</div>
</div>
)
}
const cart = cartData?.results?.cart
const items = cart?.items || []
const profile = profileData?.results?.info
if (!items.length) {
return (
<div className="mt-14 px-4 sm:px-6 lg:px-20">
<div className="flex items-center justify-center h-64">
<div className="text-lg">سبد خرید شما خالی است</div>
</div>
</div>
)
}
// محاسبه هزینه ارسال
const totalShippingCost = shippingData?.results?.shipping?.reduce((total, shop) => {
return total + (shop.shippers?.[0]?.totalShippingCost || 0)
}, 0) || 0
// محاسبه قیمت نهایی
const finalTotal = (cart?.payable_price || 0) + totalShippingCost
return (
<div className="mt-14 px-4 sm:px-6 lg:px-20">
@@ -19,25 +71,39 @@ const CartShipping: NextPage = () => {
<div>
آدرس تحویل سفارش
</div>
<div className='mt-2 text-sm text-black'>
اراک خیابان امام حسن غریبی خیابان 123
</div>
<div className='mt-2'>حمید ضرغامی</div>
{profile?.userAddress && (
<div className='mt-2 text-sm text-black'>
{profile.userAddress.address}
{profile.userAddress.city && `، ${profile.userAddress.city.name}`}
{profile.userAddress.province && `، ${profile.userAddress.province.name}`}
</div>
)}
</div>
</div>
<div className='text-primary text-sm self-end sm:self-auto'>ویرایش</div>
<Link href={'/profile'} className='text-primary text-sm self-end sm:self-auto'>ویرایش</Link>
</div>
<div className="flex-1 mt-5 border border-border p-4 sm:p-5 rounded-2xl">
<CartItem noBorder />
<CartItem />
{items.map((item: CartItemType, index: number) => {
if (!item || !item.product || !item.variant) {
return null
}
return (
<CartItem
key={`${item.product._id}-${item.variant._id}`}
item={item}
noBorder={index === 0}
/>
)
})}
</div>
</div>
<div className="w-full lg:w-[360px] lg:flex-shrink-0">
<CartSummary
itemsCount={3}
itemsPrice={580000}
discount={580000}
total={80580000}
itemsCount={cart?.items_count || 0}
itemsPrice={cart?.retail_price || 0}
discount={cart?.total_discount || 0}
shippingCost={totalShippingCost}
total={finalTotal}
confirmHref="/cart/payment"
confirmLabel="پرداخت"
className="w-full"
+136
View File
@@ -166,3 +166,139 @@ export type RemoveCartType = {
productId: number;
variantId: string;
};
// Shipping Cost Types
export type ShippingItemType = {
product: number;
variant: string;
quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shippingCost: number;
isFreeShip: boolean;
};
export type ShipperType = {
shipperId: number;
shipperName: string;
shippingDaysRange: string[];
totalShippingCost: number;
items: ShippingItemType[];
};
export type ShopShippingType = {
shopId: string;
shopName: string;
items: Array<{
product: {
_id: number;
url: string;
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[];
}>;
brand: {
_id: string;
};
category: {
_id: string;
};
variants: Array<{
_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;
};
warranty: {
_id: number;
duration: string;
logoUrl: string;
name: string;
};
meterage?: {
_id: number;
value: string;
};
size?: {
_id: number;
value: string;
};
};
quantity: number;
}>;
shippers: ShipperType[];
};
// Shipping Cost API Response Type
export type ShippingCostResponseType = {
status: number;
success: boolean;
results: {
shipping: ShopShippingType[];
};
};
// Payment Method Types
export type PaymentMethodType = {
_id: string;
title_fa: string;
title_en: string;
description: string;
provider: string;
isActive: boolean;
type: string;
createdAt: string;
updatedAt: string;
};
// Payment Methods API Response Type
export type PaymentMethodsResponseType = {
status: number;
success: boolean;
results: {
paymentMethods: PaymentMethodType[];
};
};
+12
View File
@@ -0,0 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { useSharedStore } from "@/share/store/sharedStore";
export const useGetProfile = () => {
const { isLogin } = useSharedStore();
return useQuery({
queryKey: ["profile"],
queryFn: api.getProfile,
enabled: isLogin,
});
};
+7
View File
@@ -0,0 +1,7 @@
import axios from "@/config/axios";
import { UserMeResponseType } from "@/share/types/SharedTypes";
export const getProfile = async () => {
const { data } = await axios.get<UserMeResponseType>("/user/profile");
return data;
};
+5 -1
View File
@@ -9,6 +9,7 @@ type CartSummaryProps = {
itemsCount: number;
itemsPrice: number;
discount: number;
shippingCost?: number;
total: number;
onConfirm?: () => void;
confirmHref?: string;
@@ -31,7 +32,7 @@ const Row: FC<{ label: string; value: string | number; emphasize?: boolean }> =
};
const CartSummary: FC<CartSummaryProps> = (props) => {
const { itemsCount, itemsPrice, discount, total, onConfirm, onDownloadInvoice, className, confirmHref, confirmLabel } = props;
const { itemsCount, itemsPrice, discount, shippingCost, total, onConfirm, onDownloadInvoice, className, confirmHref, confirmLabel } = props;
return (
<div className={"rounded-xl sm:rounded-2xl border border-border p-4 sm:p-6 h-fit bg-[#FAFAFA] " + (className ?? "")}>
@@ -43,6 +44,9 @@ const CartSummary: FC<CartSummaryProps> = (props) => {
<div className="mt-4 sm:mt-6 space-y-3 sm:space-y-5">
<Row label="قیمت کالاها" value={itemsPrice} />
<Row label="تخفیف" value={discount} />
{shippingCost !== undefined && shippingCost > 0 && (
<Row label="هزینه ارسال" value={shippingCost} />
)}
<Row label="جمع فاکتور" value={total} emphasize />
</div>
+5 -1
View File
@@ -6,11 +6,13 @@ import { FC, Fragment, useEffect, useState } from 'react'
import Menu from './components/Menu'
import { useSharedStore } from './store/sharedStore'
import { getToken } from '@/config/func'
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
const Header: FC = () => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const { setIsLogin } = useSharedStore()
const { data: profile } = useGetProfile()
const handleSetIsLogin = async () => {
const token = await getToken()
@@ -49,7 +51,9 @@ const Header: FC = () => {
<div className='flex items-center gap-1 sm:gap-2 md:gap-6'>
<div className='hidden md:flex h-10 border border-border py-3 px-4 rounded-[12px] items-center gap-2.5'>
<Profile size={20} color='#333333' />
<div className='text-sm text-[#333333]'>حساب کاربری</div>
{
<div className='text-sm text-[#333333]'>{profile?.results?.info?.fullName ? profile?.results?.info?.fullName : 'حساب کاربری'}</div>
}
</div>
<div className='md:hidden'>
+34 -16
View File
@@ -44,24 +44,42 @@ export type CategoryResponseType = ApiResponse<{
}>;
export type UserMeResponseType = ApiResponse<{
user: {
id: string;
info: {
_id: string;
fullName: string;
phoneNumber: string;
dateOfBirth: string | null;
address: string;
nationalCode: string | null;
homeNumber: string | null;
createdAt: string;
updatedAt: string;
firstName: string;
lastName: string;
email: string;
emailVerified: boolean;
phone: string;
birthDate: string;
nationalCode: string;
financialType: string;
profilePic: string | null;
userName: string | null;
roles: Array<{
id: string;
userAddress: {
_id: string;
address: string;
city: {
_id: number;
name: string;
province: number;
createdAt: string;
updatedAt: string;
__v: number;
};
province: {
_id: number;
name: string;
createdAt: string;
updatedAt: string;
__v: number;
};
postalCode: string;
plaque: string;
lat: string;
lon: string;
createdAt: string;
name: string;
}>;
updatedAt: string;
__v: number;
};
isSeller: boolean;
};
}>;