From c4b4780a4c8f02a84b19c24f4ecb679a537b3f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 18 Jul 2026 15:29:21 +0330 Subject: [PATCH] perf: reservation modal inputs and success reservation modified --- src/components/ReservationModal.tsx | 78 ++++++++++++++++++++++------- src/pages/CafeDetail.tsx | 1 + src/pages/PaymentSuccess.tsx | 51 +++++++++++++++---- src/utils/iranDate.ts | 13 +++++ 4 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx index b968512..39c6b29 100644 --- a/src/components/ReservationModal.tsx +++ b/src/components/ReservationModal.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import { Cafe } from "../services/campaignApi"; import { chargePageUrl, createCharge } from "../services/ipgApi"; import { fetchWalletBalances } from "../services/walletApi"; -import { formatPersianNumber } from "../utils/formatNumber"; +import { toEnglishDigits, toPersianDigits, formatPersianNumber } from "../utils/formatNumber"; import { toE164 } from "../utils/phone"; import { setPendingReservation } from "../utils/pendingReservation"; import { getUserPhone } from "../utils/userPhone"; @@ -16,6 +16,7 @@ interface ReservationModalProps { cafe: Cafe; ticketPrice: number; cafeMatchScheduleUuid: string; + remainingTickets: number; } // TODO: there's no "get the logged-in customer's own wallet uuid" endpoint yet — @@ -69,17 +70,32 @@ export default function ReservationModal({ cafe, ticketPrice, cafeMatchScheduleUuid, + remainingTickets, }: ReservationModalProps) { const navigate = useNavigate(); const [fullName, setFullName] = useState(""); + const [phone, setPhone] = useState(() => getUserPhone() ?? ""); const [count, setCount] = useState(1); const [walletBalance, setWalletBalance] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(""); + const [fieldErrors, setFieldErrors] = useState>({}); + const [payError, setPayError] = useState(""); + + const clearFieldError = (field: string) => + setFieldErrors((prev) => { + if (!prev[field]) return prev; + const next = { ...prev }; + delete next[field]; + return next; + }); useEffect(() => { if (!isOpen) return; - + setFullName(""); + setPhone(getUserPhone() ?? ""); + setCount(1); + setFieldErrors({}); + setPayError(""); const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { @@ -98,12 +114,16 @@ export default function ReservationModal({ const reservationAmount = ticketPrice * count; const payableAmount = Math.max(0, reservationAmount - walletBalance); + const atMaxCount = remainingTickets > 0 && count >= remainingTickets; const handlePay = async () => { - setError(""); + setPayError(""); - if (!fullName.trim()) { - setError("لطفاً نام و نام خانوادگی را وارد کنید."); + const errors: Record = {}; + if (!fullName.trim()) errors.name = "نام و نام خانوادگی را وارد کنید"; + if (phone.length !== 11) errors.phone = "شماره تلفن باید ۱۱ رقم باشد"; + if (Object.keys(errors).length > 0) { + setFieldErrors(errors); return; } @@ -113,7 +133,7 @@ export default function ReservationModal({ cafe_match_schedule_uuid: cafeMatchScheduleUuid, people_count: count, full_name: fullName, - phone_number: toE164(getUserPhone() ?? ""), + phone_number: toE164(phone), }; try { @@ -134,7 +154,7 @@ export default function ReservationModal({ navigate("/payment/success?state=20"); } } catch { - setError( + setPayError( "اتصال به درگاه پرداخت با خطا مواجه شد. لطفاً دوباره تلاش کنید.", ); setIsSubmitting(false); @@ -162,10 +182,15 @@ export default function ReservationModal({ setFullName(e.target.value)} + onChange={(e) => { setFullName(e.target.value); clearFieldError("name"); }} placeholder="نام و نام خانوادگی" - className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999]" + className={`w-full rounded-2xl border px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999] ${ + fieldErrors.name ? "border-red-400" : "border-gray-200" + }`} /> + {fieldErrors.name && ( +

{fieldErrors.name}

+ )}
@@ -174,11 +199,22 @@ export default function ReservationModal({ { + const digits = toEnglishDigits(e.target.value).replace(/\D/g, "").slice(0, 11); + setPhone(toPersianDigits(digits)); + clearFieldError("phone"); + }} placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹" - className="w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999]" + className={`w-full rounded-2xl border px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999] ${ + fieldErrors.phone ? "border-red-400" : "border-gray-200" + }`} /> + {fieldErrors.phone && ( +

{fieldErrors.phone}

+ )}
@@ -206,12 +242,20 @@ export default function ReservationModal({
+ {remainingTickets > 0 && ( +

+ حداکثر {formatPersianNumber(remainingTickets)} نفر +

+ )}

@@ -243,8 +287,8 @@ export default function ReservationModal({
- {error && ( -

{error}

+ {payError && ( +

{payError}

)}