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="مثال: ۰۹۱۲۳۴۵۶۷۸۹"