base logig pages cart shipping and payment
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}>;
|
||||
|
||||
Reference in New Issue
Block a user