perf: otp and cafe register fields error handling
This commit is contained in:
parent
b4c8c1a66f
commit
e6e4c690ff
2 changed files with 84 additions and 31 deletions
|
|
@ -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 (
|
||||
<div className="mt-5">
|
||||
|
|
@ -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 && (
|
||||
<p className="mt-1 text-right text-sm text-gray-500">{hint}</p>
|
||||
)}
|
||||
{error
|
||||
? <p className="mt-1 text-right text-sm text-red-500">{error}</p>
|
||||
: hint && <p className="mt-1 text-right text-sm text-gray-500">{hint}</p>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
|
@ -351,6 +358,7 @@ function PhotoUpload({
|
|||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{error && <p className="mt-1 text-right text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -359,10 +367,12 @@ function DocumentUpload({
|
|||
doc,
|
||||
onAdd,
|
||||
onRemove,
|
||||
error,
|
||||
}: {
|
||||
doc: UploadedDocument | null;
|
||||
onAdd: (file: File) => void;
|
||||
onRemove: () => void;
|
||||
error?: string;
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
|
@ -442,6 +452,7 @@ function DocumentUpload({
|
|||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{error && <p className="mt-1 text-right text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -494,7 +505,16 @@ export default function CafeOwnerRegister() {
|
|||
const [photos, setPhotos] = useState<UploadedPhoto[]>([]);
|
||||
const [licenseDoc, setLicenseDoc] = useState<UploadedDocument | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
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<string, string> = {};
|
||||
|
||||
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}
|
||||
/>
|
||||
<CitySelectField
|
||||
value={effectiveCityUuid}
|
||||
|
|
@ -746,7 +776,8 @@ export default function CafeOwnerRegister() {
|
|||
label="آدرس"
|
||||
placeholder="موقعیت کافه را با جزئیات وارد کنید"
|
||||
value={address}
|
||||
onChange={setAddress}
|
||||
onChange={(v) => { setAddress(v); clearFieldError("address"); }}
|
||||
error={fieldErrors.address}
|
||||
/>
|
||||
<CafeLocationMap
|
||||
label="موقعیت روی نقشه"
|
||||
|
|
@ -758,18 +789,21 @@ export default function CafeOwnerRegister() {
|
|||
placeholder={`\u2066۰۲۱-۱۲۳۴۵۶۷۸\u2069 :مثال`}
|
||||
hint="شماره را همراه با پیششماره شهر وارد کنید."
|
||||
value={cafePhone}
|
||||
onChange={setCafePhone}
|
||||
onChange={(v) => { setCafePhone(v); clearFieldError("cafePhone"); }}
|
||||
inputDir="ltr"
|
||||
error={fieldErrors.cafePhone}
|
||||
/>
|
||||
<PhotoUpload
|
||||
photos={photos}
|
||||
onAdd={handleAddPhotos}
|
||||
onAdd={(files) => { clearFieldError("photos"); handleAddPhotos(files); }}
|
||||
onRemove={handleRemovePhoto}
|
||||
error={fieldErrors.photos}
|
||||
/>
|
||||
<DocumentUpload
|
||||
doc={licenseDoc}
|
||||
onAdd={handleAddLicense}
|
||||
onAdd={(file) => { clearFieldError("licenseDoc"); handleAddLicense(file); }}
|
||||
onRemove={handleRemoveLicense}
|
||||
error={fieldErrors.licenseDoc}
|
||||
/>
|
||||
|
||||
<div className="mt-5">
|
||||
|
|
@ -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"); }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{fieldErrors.features && (
|
||||
<p className="mt-1 text-right text-sm text-red-500">{fieldErrors.features}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mt-4 text-right text-sm text-red-500">{error}</p>
|
||||
{submitError && (
|
||||
<p className="mt-4 text-right text-sm text-red-500">{submitError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string[]> };
|
||||
}
|
||||
|
||||
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="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue