Fix auth login flow, Iran Yekan/Doran fonts, and account tiles.

Prevent stale /auth/me from wiping a fresh session, unblock login submit, allow localhost HMR, and drop the tile "مشاهده" CTA.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-26 11:10:37 +03:30
co-authored by Cursor
parent 27d5cd0186
commit 68365457a1
24 changed files with 1294 additions and 203 deletions
+87 -86
View File
@@ -1,12 +1,11 @@
"use client";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import {
FormEvent,
InputHTMLAttributes,
ReactNode,
Suspense,
useState,
} from "react";
import ManaLogo from "@/components/ui/ManaLogo";
@@ -18,7 +17,6 @@ type LoginMethod = "password" | "otp";
function AuthFormInner() {
const router = useRouter();
const searchParams = useSearchParams();
const {
login,
sendLoginOtp,
@@ -35,24 +33,28 @@ function AuthFormInner() {
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const isLogin = mode === "login";
const customerHint = searchParams.get("as") === "customer";
function afterAuth(role: string) {
if (role === "CUSTOMER") {
setError(
"این پنل مخصوص فروشندگان است. مشتریان از وب‌سایت مانا کالکشن وارد شوند.",
);
return;
}
router.replace("/");
function afterAuth(_role: string) {
router.replace("/account");
}
async function handleLoginPassword(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
async function handleLoginPassword(event?: FormEvent<HTMLFormElement>) {
event?.preventDefault();
event?.stopPropagation();
setError(null);
setBusy(true);
const form = new FormData(event.currentTarget);
const cellNumber = String(form.get("cellNumber") ?? "").trim();
const formEl =
event?.currentTarget ??
(document.getElementById("auth-login-form") as HTMLFormElement | null);
if (!formEl) {
setError("فرم ورود پیدا نشد");
setBusy(false);
return;
}
const form = new FormData(formEl);
const cellNumber = String(form.get("cellNumber") ?? "")
.trim()
.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)));
const password = String(form.get("password") ?? "");
try {
const user = await login(cellNumber, password);
@@ -64,12 +66,23 @@ function AuthFormInner() {
}
}
async function handleLoginOtp(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
async function handleLoginOtp(event?: FormEvent<HTMLFormElement>) {
event?.preventDefault();
event?.stopPropagation();
setError(null);
setBusy(true);
const form = new FormData(event.currentTarget);
const cellNumber = String(form.get("cellNumber") ?? "").trim();
const formEl =
event?.currentTarget ??
(document.getElementById("auth-login-form") as HTMLFormElement | null);
if (!formEl) {
setError("فرم ورود پیدا نشد");
setBusy(false);
return;
}
const form = new FormData(formEl);
const cellNumber = String(form.get("cellNumber") ?? "")
.trim()
.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)));
const code = String(form.get("code") ?? "").trim();
try {
if (!otpSent) {
@@ -87,14 +100,25 @@ function AuthFormInner() {
}
}
async function handleRegister(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
async function handleRegister(event?: FormEvent<HTMLFormElement>) {
event?.preventDefault();
event?.stopPropagation();
setError(null);
setBusy(true);
const form = new FormData(event.currentTarget);
const formEl =
event?.currentTarget ??
(document.getElementById("auth-register-form") as HTMLFormElement | null);
if (!formEl) {
setError("فرم ثبت‌نام پیدا نشد");
setBusy(false);
return;
}
const form = new FormData(formEl);
const firstName = String(form.get("firstName") ?? "").trim();
const lastName = String(form.get("lastName") ?? "").trim();
const cellNumber = String(form.get("cellNumber") ?? "").trim();
const cellNumber = String(form.get("cellNumber") ?? "")
.trim()
.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d)));
const password = String(form.get("password") ?? "");
const confirm = String(form.get("confirm") ?? "");
const code = String(form.get("code") ?? "").trim();
@@ -176,29 +200,16 @@ function AuthFormInner() {
</button>
</div>
{customerHint ? (
<p className="mb-4 text-xs leading-6 text-muted">
حساب شما مشتری است. برای مدیریت فروشگاه باید با حساب فروشنده وارد
شوید.
</p>
) : null}
{error ? (
<p className="mb-4 text-xs leading-6 text-red-600">{error}</p>
) : null}
<div className="grid min-h-0 flex-1">
<div
className={`col-start-1 row-start-1 flex items-center transition-opacity duration-300 ${
isLogin
? "relative z-10 opacity-100"
: "pointer-events-none invisible z-0 opacity-0"
}`}
aria-hidden={!isLogin}
>
<div className="flex min-h-0 flex-1 items-center">
{isLogin ? (
<AuthPanel
title="ورود به وبسایت"
subtitle="با شماره موبایل و رمز عبور یا کد یکبارمصرف وارد پنل فروشگاه شوید."
formId="auth-login-form"
title="ورود به حساب"
subtitle="با شماره موبایل و رمز عبور یا کد یکبارمصرف وارد شوید."
onSubmit={
loginMethod === "password"
? handleLoginPassword
@@ -215,7 +226,7 @@ function AuthFormInner() {
placeholder="09123456789"
dir="ltr"
pattern="09[0-9]{9}"
required={isLogin}
required
defaultValue={pendingCell || undefined}
/>
{loginMethod === "password" ? (
@@ -227,7 +238,7 @@ function AuthFormInner() {
autoComplete="off"
placeholder="••••••••"
dir="ltr"
required={isLogin}
required
minLength={4}
/>
) : otpSent ? (
@@ -246,10 +257,14 @@ function AuthFormInner() {
/>
) : null}
<button
type="submit"
type="button"
className="auth-button"
disabled={busy}
tabIndex={isLogin ? 0 : -1}
onClick={() => {
void (loginMethod === "password"
? handleLoginPassword()
: handleLoginOtp());
}}
>
<span>
{busy
@@ -298,17 +313,9 @@ function AuthFormInner() {
</button>
</div>
</AuthPanel>
</div>
<div
className={`col-start-1 row-start-1 flex items-center transition-opacity duration-300 ${
!isLogin
? "relative z-10 opacity-100"
: "pointer-events-none invisible z-0 opacity-0"
}`}
aria-hidden={isLogin}
>
) : (
<AuthPanel
formId="auth-register-form"
title="عضویت در مانا کالکشن"
subtitle="با تکمیل اطلاعات زیر، ما یک حساب کاربری برای شما ایجاد میکنیم."
onSubmit={handleRegister}
@@ -322,7 +329,7 @@ function AuthFormInner() {
name="firstName"
type="text"
autoComplete="off"
required={!isLogin}
required
/>
<Field
id="register-last"
@@ -330,7 +337,7 @@ function AuthFormInner() {
name="lastName"
type="text"
autoComplete="off"
required={!isLogin}
required
/>
</div>
<Field
@@ -343,7 +350,7 @@ function AuthFormInner() {
placeholder="09123456789"
dir="ltr"
pattern="09[0-9]{9}"
required={!isLogin}
required
/>
<Field
id="register-password"
@@ -353,7 +360,7 @@ function AuthFormInner() {
autoComplete="new-password"
placeholder="••••••••"
dir="ltr"
required={!isLogin}
required
minLength={4}
/>
<Field
@@ -364,7 +371,7 @@ function AuthFormInner() {
autoComplete="new-password"
placeholder="••••••••"
dir="ltr"
required={!isLogin}
required
minLength={4}
/>
</>
@@ -395,10 +402,12 @@ function AuthFormInner() {
</>
)}
<button
type="submit"
type="button"
className="auth-button"
disabled={busy}
tabIndex={!isLogin ? 0 : -1}
onClick={() => {
void handleRegister();
}}
>
<span>
{busy
@@ -409,7 +418,7 @@ function AuthFormInner() {
</span>
</button>
</AuthPanel>
</div>
)}
</div>
<p className="mt-6 shrink-0 text-xs leading-6 text-muted">
@@ -450,36 +459,36 @@ function AuthFormInner() {
}
export default function AuthForm() {
return (
<Suspense
fallback={
<div className="flex min-h-dvh items-center justify-center text-sm text-muted">
در حال بارگذاری…
</div>
}
>
<AuthFormInner />
</Suspense>
);
return <AuthFormInner />;
}
function AuthPanel({
formId,
title,
subtitle,
onSubmit,
children,
}: {
formId: string;
title: string;
subtitle: string;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
children: ReactNode;
}) {
return (
<div className="w-full">
<h1 className="mb-2 text-xl font-medium text-ink sm:text-2xl">{title}</h1>
<h1 className="font-doran mb-2 text-xl font-medium text-ink sm:text-2xl">
{title}
</h1>
<p className="mb-8 max-w-sm text-sm leading-7 text-muted">{subtitle}</p>
<form
onSubmit={onSubmit}
id={formId}
noValidate
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
onSubmit(event);
}}
className="flex w-full flex-col gap-4"
autoComplete="off"
>
@@ -493,14 +502,11 @@ function Field({
id,
label,
autoComplete = "off",
onFocus,
...props
}: {
id: string;
label: string;
} & InputHTMLAttributes<HTMLInputElement>) {
const [unlockAutofill, setUnlockAutofill] = useState(false);
return (
<div className="w-full">
<label htmlFor={id} className="auth-label">
@@ -511,11 +517,6 @@ function Field({
className="auth-input"
{...props}
autoComplete={autoComplete}
readOnly={!unlockAutofill}
onFocus={(event) => {
if (!unlockAutofill) setUnlockAutofill(true);
onFocus?.(event);
}}
/>
</div>
);