add coupon + success + failed Page

This commit is contained in:
hamid zarghami
2025-10-29 10:41:57 +03:30
parent cc23cf86e6
commit bf0a770ee0
7 changed files with 244 additions and 51 deletions
+42 -23
View File
@@ -1,32 +1,50 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import Input from '@/components/Input' import Input from '@/components/Input'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useAddCoupon, useGetCart } from '../hooks/useCartData'
import { toast } from '@/components/Toast'
import { extractErrorMessage } from '@/helpers/errorUtils'
type DiscountCardProps = {
discountCode: string
setDiscountCode: (code: string) => void
onSubmit: () => void
isLoading?: boolean
}
const DiscountCard: FC<DiscountCardProps> = ({
discountCode, const DiscountCard: FC = () => {
setDiscountCode,
onSubmit,
isLoading = false
}) => {
const [isExpanded, setIsExpanded] = useState(false) const [isExpanded, setIsExpanded] = useState(false)
const [couponCode, setCouponCode] = useState('')
const addCouponMutation = useAddCoupon()
const { data: cartData, refetch } = useGetCart()
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
onSubmit()
if (!couponCode.trim()) {
toast('لطفاً کد کوپن را وارد کنید', 'error')
return
} }
await addCouponMutation.mutateAsync({
code: couponCode,
cartShipmentItemId: cartData?.results?.cart?.cartShipmentItems?.[0]._id as string
}, {
onSuccess: () => {
toast('کوپن با موفقیت اعمال شد', 'success')
refetch()
setCouponCode('')
setIsExpanded(false)
},
onError: (error) => {
toast(extractErrorMessage(error), 'error')
}
})
}
if (!isExpanded) { if (!isExpanded) {
return ( return (
<div className='h-16 border border-border rounded-2xl flex justify-between mt-5 items-center px-6'> <div className='h-16 border border-border rounded-2xl flex justify-between mt-5 items-center px-6'>
<div> <div>
کد تخفیف دارید؟ کد کوپن دارید؟
</div> </div>
<Button <Button
@@ -42,32 +60,33 @@ const DiscountCard: FC<DiscountCardProps> = ({
return ( return (
<div className='border border-border rounded-2xl mt-5 p-4 sm:p-6'> <div className='border border-border rounded-2xl mt-5 p-4 sm:p-6'>
<div className='text-sm font-medium mb-4'>کد تخفیف</div> <div className='text-sm font-medium mb-4'>کد کوپن</div>
<form onSubmit={handleSubmit} className='flex gap-3'> <form onSubmit={handleSubmit} className='flex gap-3'>
<Input <Input
type="text" type="text"
placeholder="کد تخفیف خود را وارد کنید" placeholder="کد کوپن خود را وارد کنید"
value={discountCode} value={couponCode}
onChange={(e) => setDiscountCode(e.target.value)} onChange={(e) => setCouponCode(e.target.value)}
className="flex-1" className="flex-1"
disabled={isLoading} disabled={addCouponMutation.isPending}
label="" label=""
/> />
<Button <Button
type="submit" type="submit"
disabled={isLoading || !discountCode.trim()} disabled={addCouponMutation.isPending || !couponCode.trim()}
className="px-6" className="px-6"
> >
{isLoading ? 'در حال اعمال...' : 'اعمال'} {addCouponMutation.isPending ? 'در حال اعمال...' : 'اعمال'}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setIsExpanded(false) setIsExpanded(false)
setDiscountCode('') setCouponCode('')
}} }}
disabled={addCouponMutation.isPending}
className="px-4" className="px-4"
> >
لغو لغو
+6
View File
@@ -110,3 +110,9 @@ export const useSaveShipmentCart = () => {
mutationFn: api.saveShipmentCart, mutationFn: api.saveShipmentCart,
}); });
}; };
export const useAddCoupon = () => {
return useMutation({
mutationFn: api.addCoupon,
});
};
+5 -26
View File
@@ -5,7 +5,7 @@ import { NextPage } from 'next'
import TitleBack from '../components/TitleBack' import TitleBack from '../components/TitleBack'
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import DiscountCard from '../components/DiscountCard' import DiscountCard from '../components/DiscountCard'
import { usePaymentMethods, useGetCart, useShippingCost, useApplyDiscountCode, useCheckoutCart } from '../hooks/useCartData' import { usePaymentMethods, useGetCart, useShippingCost, useCheckoutCart } from '../hooks/useCartData'
import { useState } from 'react' import { useState } from 'react'
import { toast } from '@/components/Toast' import { toast } from '@/components/Toast'
import { useGetProfile } from '@/app/profile/hooks/useProfileData' import { useGetProfile } from '@/app/profile/hooks/useProfileData'
@@ -16,11 +16,9 @@ const CartPayment: NextPage = () => {
const { data: shippingData, isLoading: shippingLoading } = useShippingCost() const { data: shippingData, isLoading: shippingLoading } = useShippingCost()
const { data: profileData, isLoading: profileLoading } = useGetProfile() const { data: profileData, isLoading: profileLoading } = useGetProfile()
const applyDiscountMutation = useApplyDiscountCode()
const checkoutMutation = useCheckoutCart() const checkoutMutation = useCheckoutCart()
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('') const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('')
const [discountCode, setDiscountCode] = useState<string>('')
const isLoading = paymentMethodsLoading || cartLoading || shippingLoading || profileLoading const isLoading = paymentMethodsLoading || cartLoading || shippingLoading || profileLoading
@@ -66,22 +64,6 @@ const CartPayment: NextPage = () => {
setSelectedPaymentMethod(value) setSelectedPaymentMethod(value)
} }
const handleDiscountCodeSubmit = async () => {
if (!discountCode.trim()) {
toast('لطفاً کد تخفیف را وارد کنید', 'error')
return
}
try {
await applyDiscountMutation.mutateAsync({ discount_code: discountCode })
toast('کد تخفیف با موفقیت اعمال شد', 'success')
setDiscountCode('')
} catch (error: unknown) {
const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'کد تخفیف نامعتبر است'
toast(errorMessage, 'error')
}
}
const handlePayment = async () => { const handlePayment = async () => {
if (!selectedPaymentMethod) { if (!selectedPaymentMethod) {
toast('لطفاً روش پرداخت را انتخاب کنید', 'error') toast('لطفاً روش پرداخت را انتخاب کنید', 'error')
@@ -109,12 +91,9 @@ const CartPayment: NextPage = () => {
<div className="mt-6 flex flex-col lg:flex-row gap-6"> <div className="mt-6 flex flex-col lg:flex-row gap-6">
<div className='flex-1'> <div className='flex-1'>
<TitleBack title='پرداخت' /> <TitleBack title='پرداخت' />
<DiscountCard {shippingData?.results?.shipping?.map((shop) => (
discountCode={discountCode} <DiscountCard key={shop.shopId} />
setDiscountCode={setDiscountCode} ))}
onSubmit={handleDiscountCodeSubmit}
isLoading={applyDiscountMutation.isPending}
/>
<div className='mt-5 border border-border rounded-2xl p-4 sm:p-6 font-light'> <div className='mt-5 border border-border rounded-2xl p-4 sm:p-6 font-light'>
<div>انتخاب روش پرداخت</div> <div>انتخاب روش پرداخت</div>
@@ -151,7 +130,7 @@ const CartPayment: NextPage = () => {
<CartSummary <CartSummary
itemsCount={cart?.items_count || 0} itemsCount={cart?.items_count || 0}
itemsPrice={cart?.retail_price || 0} itemsPrice={cart?.retail_price || 0}
discount={cart?.total_discount || 0} discount={cart.total_discount}
shippingCost={totalShippingCost} shippingCost={totalShippingCost}
total={finalTotal} total={finalTotal}
onConfirm={handlePayment} onConfirm={handlePayment}
+6
View File
@@ -15,6 +15,7 @@ import {
BulkAddCartType, BulkAddCartType,
ShipmentCartResponseType, ShipmentCartResponseType,
ShipmentSaveType, ShipmentSaveType,
AddCouponType,
} from "../types/Types"; } from "../types/Types";
export const getCart = async (): Promise<CartResponseType> => { export const getCart = async (): Promise<CartResponseType> => {
@@ -87,3 +88,8 @@ export const saveShipmentCart = async (params: ShipmentSaveType) => {
const { data } = await axios.post("/shipment/save", params); const { data } = await axios.post("/shipment/save", params);
return data; return data;
}; };
export const addCoupon = async (params: AddCouponType) => {
const { data } = await axios.post("/coupon/add-coupon", params);
return data;
};
+36 -1
View File
@@ -79,6 +79,35 @@ export type CartItemType = {
quantity: number; quantity: number;
}; };
// Cart Shipment Item Type
export type CartShipmentItemType = {
_id: string;
cart: string;
shop: string;
shipmentItems: Array<{
product: number;
variant: string;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
isFreeShip: boolean;
_id: string;
}>;
shipper: number;
totalSellingPrice: number;
totalRetailPrice: number;
totalShipmentCost: number;
totalCouponDiscount: number;
totalPaymentPrice: number;
itemsCount: number;
createdAt: string;
updatedAt: string;
};
// Cart Types // Cart Types
export type CartType = { export type CartType = {
_id: string; _id: string;
@@ -95,11 +124,12 @@ export type CartType = {
dateOfBirth: string | null; dateOfBirth: string | null;
address: { address: {
_id: string; _id: string;
}; } | null;
nationalCode: string | null; nationalCode: string | null;
homeNumber: string | null; homeNumber: string | null;
}; };
isWholeSale: boolean; isWholeSale: boolean;
cartShipmentItems: CartShipmentItemType[];
}; };
// Recommended Product Type // Recommended Product Type
@@ -433,3 +463,8 @@ export type ShipmentCartResponseType = {
recommended: RecommendedProductType[]; recommended: RecommendedProductType[];
}; };
}; };
export type AddCouponType = {
code: string;
cartShipmentItemId: string;
};
+148
View File
@@ -0,0 +1,148 @@
'use client'
import Layout from '@/hoc/Layout'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { TickCircle, CloseCircle } from 'iconsax-react'
import { Suspense } from 'react'
const ThankYouContent = () => {
const searchParams = useSearchParams()
const status = searchParams.get('status')
const orderId = searchParams.get('order_id')
const paymentId = searchParams.get('payment_id')
// صفحه پرداخت ناموفق
if (status === 'failed') {
return (
<div className="flex items-center justify-center min-h-[60vh] px-4 py-8">
<div className="text-center max-w-lg w-full">
{/* آیکون خطا */}
<div className="flex justify-center mb-6">
<div className="bg-red-50 rounded-full p-5">
<CloseCircle color="red" size={64} className="text-red-500" variant="Bold" />
</div>
</div>
{/* پیام اصلی */}
<h1 className="text-2xl font-bold text-gray-900 mb-2">
پرداخت ناموفق بود
</h1>
<p className="text-gray-600 mb-8">
متأسفانه پرداخت شما با خطا مواجه شد
</p>
{/* اطلاعات */}
{orderId && (
<div className="bg-gray-50 rounded-xl p-6 mb-6 text-right">
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">شماره سفارش:</span>
<span className="font-bold text-gray-900">{orderId}</span>
</div>
{paymentId && (
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<span className="text-sm text-gray-600">کد پیگیری:</span>
<span className="text-sm text-gray-800 font-mono">{paymentId}</span>
</div>
)}
</div>
</div>
)}
{/* دکمه‌های اکشن */}
<div className="flex flex-col gap-3">
<Button asChild className="w-full">
<Link href="/cart/payment">تلاش مجدد برای پرداخت</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href="/">بازگشت به صفحه اصلی</Link>
</Button>
</div>
</div>
</div>
)
}
// صفحه پرداخت موفق
if (status === 'success') {
return (
<div className="flex items-center justify-center min-h-[60vh] px-4 py-8">
<div className="text-center max-w-lg w-full">
{/* آیکون موفقیت */}
<div className="flex justify-center mb-6">
<div className="bg-green-50 rounded-full p-5">
<TickCircle size={64} className="text-green-500" variant="Bold" />
</div>
</div>
{/* پیام اصلی */}
<h1 className="text-2xl font-bold text-gray-900 mb-2">
پرداخت با موفقیت انجام شد
</h1>
<p className="text-gray-600 mb-8">
سفارش شما ثبت شد و به زودی ارسال میشود
</p>
{/* اطلاعات سفارش */}
<div className="bg-gray-50 rounded-xl p-6 mb-6 text-right">
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">شماره سفارش:</span>
<span className="font-bold text-gray-900">{orderId}</span>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<span className="text-sm text-gray-600">کد پیگیری:</span>
<span className="text-sm text-gray-800 font-mono">{paymentId}</span>
</div>
</div>
</div>
{/* دکمه‌های اکشن */}
<div className="flex flex-col gap-3">
<Button asChild className="w-full">
<Link href="/profile/orders">مشاهده سفارشات</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href="/">بازگشت به صفحه اصلی</Link>
</Button>
</div>
</div>
</div>
)
}
// در صورتی که status معتبر نباشد
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center max-w-md w-full px-4">
<div className="text-gray-600 text-lg mb-4">
اطلاعات پرداخت نامعتبر است
</div>
<Button asChild>
<Link href="/">بازگشت به صفحه اصلی</Link>
</Button>
</div>
</div>
)
}
const ThankYouPage = () => {
return (
<Layout>
<Suspense
fallback={
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
}
>
<ThankYouContent />
</Suspense>
</Layout>
)
}
export default ThankYouPage
+1 -1
View File
@@ -145,7 +145,7 @@ const Cart = () => {
</div> </div>
{/* View Cart Button */} {/* View Cart Button */}
{itemsCount > 0 && ( {itemsCount > 0 && isLogin && (
<Link href="/cart" className='block'> <Link href="/cart" className='block'>
<Button className='w-full bg-primary hover:bg-primary/90 text-white rounded-lg py-3'> <Button className='w-full bg-primary hover:bg-primary/90 text-white rounded-lg py-3'>
مشاهده سبد خرید مشاهده سبد خرید