From e6e4c690ff6ee4934c8c60d4e7f22879a1d63b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 18 Jul 2026 15:01:48 +0330 Subject: [PATCH] perf: otp and cafe register fields error handling --- src/pages/CafeOwnerRegister.tsx | 89 +++++++++++++++++++++++---------- src/pages/Login.tsx | 26 ++++++++-- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/pages/CafeOwnerRegister.tsx b/src/pages/CafeOwnerRegister.tsx index a8abd45..6f296b8 100644 --- a/src/pages/CafeOwnerRegister.tsx +++ b/src/pages/CafeOwnerRegister.tsx @@ -36,6 +36,7 @@ function FormField({ value, onChange, inputDir = "ltr", + error, }: { label: string; placeholder: string; @@ -43,6 +44,7 @@ function FormField({ value: string; onChange: (value: string) => void; inputDir?: "ltr" | "rtl"; + error?: string; }) { return (
@@ -55,11 +57,14 @@ function FormField({ value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} - className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right h-14 placeholder:font-light placeholder:text-[#999999]" + className={`w-full rounded-2xl border px-4 py-3 text-right h-14 placeholder:font-light placeholder:text-[#999999] ${ + error ? "border-red-400" : "border-gray-200" + }`} /> - {hint && ( -

{hint}

- )} + {error + ?

{error}

+ : hint &&

{hint}

+ }
); } @@ -253,10 +258,12 @@ function PhotoUpload({ photos, onAdd, onRemove, + error, }: { photos: UploadedPhoto[]; onAdd: (files: File[]) => void; onRemove: (id: string) => void; + error?: string; }) { const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); @@ -351,6 +358,7 @@ function PhotoUpload({ e.target.value = ""; }} /> + {error &&

{error}

} ); } @@ -359,10 +367,12 @@ function DocumentUpload({ doc, onAdd, onRemove, + error, }: { doc: UploadedDocument | null; onAdd: (file: File) => void; onRemove: () => void; + error?: string; }) { const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); @@ -442,6 +452,7 @@ function DocumentUpload({ e.target.value = ""; }} /> + {error &&

{error}

} ); } @@ -494,7 +505,16 @@ export default function CafeOwnerRegister() { const [photos, setPhotos] = useState([]); const [licenseDoc, setLicenseDoc] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(""); + const [fieldErrors, setFieldErrors] = useState>({}); + const [submitError, setSubmitError] = useState(""); + + const clearFieldError = (field: string) => + setFieldErrors((prev) => { + if (!prev[field]) return prev; + const next = { ...prev }; + delete next[field]; + return next; + }); // keep refs so the unmount cleanup can revoke any remaining object URLs const photosRef = useRef(photos); @@ -662,24 +682,33 @@ export default function CafeOwnerRegister() { }; const handleContinue = async () => { + const errors: Record = {}; + + if (!cafeName.trim()) errors.cafeName = "نام کافه الزامی است"; + if (!address.trim()) errors.address = "آدرس الزامی است"; + if (!cafePhone.trim()) errors.cafePhone = "شماره تماس الزامی است"; + if (selectedFeatures.length === 0) errors.features = "حداقل یک ویژگی انتخاب کنید"; + const stillUploading = photos.some((p) => p.status === "uploading") || licenseDoc?.status === "uploading"; + if (stillUploading) { - setError("لطفاً منتظر بمانید تا آپلود فایل‌ها کامل شود."); + errors.photos = "لطفاً منتظر بمانید تا آپلود فایل‌ها کامل شود"; + } else { + if (!photos.some((p) => p.status === "done")) + errors.photos = "حداقل یک تصویر از کافه الزامی است"; + if (!licenseDoc || licenseDoc.status !== "done") + errors.licenseDoc = "تصویر مجوز کسب و کار الزامی است"; + } + + if (Object.keys(errors).length > 0) { + setFieldErrors(errors); return; } - if (!isEditMode) { - if (!photos.some((p) => p.status === "done")) { - setError("لطفاً حداقل یک تصویر برای کافه آپلود کنید."); - return; - } - if (!licenseDoc || licenseDoc.status !== "done") { - setError("لطفاً تصویر مجوز کسب و کار را آپلود کنید."); - return; - } - } - setError(""); + + setFieldErrors({}); + setSubmitError(""); setIsSubmitting(true); try { const mediaItems: CafeMediaItem[] = [ @@ -719,7 +748,7 @@ export default function CafeOwnerRegister() { if (!isEditMode) { navigate("/owner/register/fail"); } else { - setError("ذخیره تغییرات با خطا مواجه شد. لطفاً دوباره تلاش کنید."); + setSubmitError("ذخیره تغییرات با خطا مواجه شد. لطفاً دوباره تلاش کنید."); } } finally { setIsSubmitting(false); @@ -735,7 +764,8 @@ export default function CafeOwnerRegister() { label="نام کافه" placeholder="نام کافه را وارد کنید" value={cafeName} - onChange={setCafeName} + onChange={(v) => { setCafeName(v); clearFieldError("cafeName"); }} + error={fieldErrors.cafeName} /> { setAddress(v); clearFieldError("address"); }} + error={fieldErrors.address} /> { setCafePhone(v); clearFieldError("cafePhone"); }} inputDir="ltr" + error={fieldErrors.cafePhone} /> { clearFieldError("photos"); handleAddPhotos(files); }} onRemove={handleRemovePhoto} + error={fieldErrors.photos} /> { clearFieldError("licenseDoc"); handleAddLicense(file); }} onRemove={handleRemoveLicense} + error={fieldErrors.licenseDoc} />
@@ -782,14 +816,17 @@ export default function CafeOwnerRegister() { key={feature.uuid} label={feature.name} checked={selectedFeatures.includes(feature.uuid)} - onToggle={() => toggleFeature(feature.uuid)} + onToggle={() => { toggleFeature(feature.uuid); clearFieldError("features"); }} /> ))}
+ {fieldErrors.features && ( +

{fieldErrors.features}

+ )} - {error && ( -

{error}

+ {submitError && ( +

{submitError}

)} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 7542bab..84e5185 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -2,10 +2,21 @@ import { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import Icon from "../components/Icon"; import { requestOtp } from "../services/authApi"; -import { toPersianDigits } from "../utils/formatNumber"; +import { toEnglishDigits, toPersianDigits } from "../utils/formatNumber"; import { toE164 } from "../utils/phone"; import { setUserRole } from "../utils/role"; +interface ApiErrorResponse { + status_message?: string; + details?: { message?: Record }; +} + +function extractApiError(error: unknown): string { + const data = (error as { response?: { data?: ApiErrorResponse } })?.response?.data; + if (data?.status_message) return data.status_message; + return "ارسال کد با خطا مواجه شد. لطفاً دوباره تلاش کنید."; +} + export default function Login() { const navigate = useNavigate(); const location = useLocation(); @@ -21,16 +32,20 @@ export default function Login() { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(""); - const canContinue = phone.trim().length > 0 && acceptedTerms && !isSubmitting; + const canContinue = phone.length === 11 && acceptedTerms && !isSubmitting; const handleContinue = async () => { setError(""); + if (phone.length !== 11) { + setError("شماره تلفن باید ۱۱ رقم باشد."); + return; + } setIsSubmitting(true); try { const { ttl } = await requestOtp(toE164(phone)); navigate("/login/otp", { state: { phone, ttl, role: isOwnerFlow ? "cafe_owner" : "customer" } }); - } catch { - setError("ارسال کد با خطا مواجه شد. لطفاً دوباره تلاش کنید."); + } catch (err) { + setError(extractApiError(err)); } finally { setIsSubmitting(false); } @@ -58,7 +73,8 @@ export default function Login() { enterKeyHint="go" value={phone} onChange={(e) => { - setPhone(toPersianDigits(e.target.value)); + const digits = toEnglishDigits(e.target.value).replace(/\D/g, "").slice(0, 11); + setPhone(toPersianDigits(digits)); setError(""); }} placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"