528 lines
26 KiB
TypeScript
528 lines
26 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { registerUser, loginUser } from "@/public/src/services/auth/api";
|
||
|
||
// لطفاً آدرس ایمپورتهای زیر را بر اساس ساختار پروژه خود اصلاح کنید
|
||
// import FloatingInput from "@/components/FloatingInput";
|
||
// import IranPhoneAdornment from "@/components/IranPhoneAdornment";
|
||
|
||
interface AuthModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
// توابع کالبک برای زمانی که لاگین یا ثبتنام موفق بود (برای آپدیت استیتهای هدر یا هدایت کاربر)
|
||
onLoginSuccess?: (user: { username: string; displayName: string }) => void;
|
||
onRegisterSuccess?: (user: { username: string; displayName: string }) => void;
|
||
}
|
||
type FloatingInputProps = {
|
||
name?: string;
|
||
value: string;
|
||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||
label: string;
|
||
type?: string;
|
||
dir?: "rtl" | "ltr";
|
||
inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"];
|
||
autoComplete?: string;
|
||
className?: string;
|
||
leftSlot?: React.ReactNode;
|
||
rightSlot?: React.ReactNode;
|
||
};
|
||
|
||
function FloatingInput({
|
||
name,
|
||
value,
|
||
onChange,
|
||
label,
|
||
type = "text",
|
||
dir = "rtl",
|
||
inputMode,
|
||
autoComplete,
|
||
className = "",
|
||
leftSlot,
|
||
rightSlot,
|
||
}: FloatingInputProps) {
|
||
const hasLeftSlot = Boolean(leftSlot);
|
||
const hasRightSlot = Boolean(rightSlot);
|
||
|
||
return (
|
||
<label className="relative block w-full">
|
||
{leftSlot ? (
|
||
<div className="pointer-events-none absolute left-4 top-1/2 z-10 -translate-y-1/2">
|
||
{leftSlot}
|
||
</div>
|
||
) : null}
|
||
{rightSlot ? (
|
||
<div className="absolute right-4 top-1/2 z-10 -translate-y-1/2">
|
||
{rightSlot}
|
||
</div>
|
||
) : null}
|
||
<input
|
||
name={name}
|
||
value={value}
|
||
onChange={onChange}
|
||
type={type}
|
||
dir={dir}
|
||
inputMode={inputMode}
|
||
autoComplete={autoComplete}
|
||
placeholder=" "
|
||
className={`peer w-full rounded-2xl border border-gray-200 bg-gray-50 py-4 text-sm text-[#1A2332] outline-none transition placeholder:text-gray-400 focus:border-[#ffb900] focus:bg-white ${hasLeftSlot ? "pl-24" : "pl-4"} ${hasRightSlot ? "pr-12" : "pr-4"} ${className}`}
|
||
/>
|
||
<span
|
||
className={`pointer-events-none absolute top-4 z-10 origin-right rounded-full bg-white px-2 text-sm text-gray-400 transition-all duration-200 peer-focus:top-0 peer-focus:-translate-y-1/2 peer-focus:scale-90 peer-focus:text-[#1A2332] peer-[:not(:placeholder-shown)]:top-0 peer-[:not(:placeholder-shown)]:-translate-y-1/2 peer-[:not(:placeholder-shown)]:scale-90 ${hasRightSlot ? "right-12" : "right-3"}`}
|
||
>
|
||
{label}
|
||
</span>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
function IranPhoneAdornment() {
|
||
return (
|
||
<div dir="ltr" className="flex items-center gap-2 text-sm text-gray-500">
|
||
<span className="text-base leading-none">🇮🇷</span>
|
||
<span className="font-medium">+98</span>
|
||
</div>
|
||
);
|
||
}
|
||
export default function AuthModal({ isOpen, onClose, onLoginSuccess, onRegisterSuccess }: AuthModalProps) {
|
||
const router = useRouter();
|
||
|
||
const [formType, setFormType] = useState("mobile");
|
||
const [activeTab, setActiveTab] = useState("login");
|
||
const [registerError, setRegisterError] = useState("");
|
||
const [loginError, setLoginError] = useState("");
|
||
const [registerUsernameError, setRegisterUsernameError] = useState("");
|
||
const [loginUsernameError, setLoginUsernameError] = useState("");
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [showLoginPassword, setShowLoginPassword] = useState(false);
|
||
const [loginMobile, setLoginMobile] = useState("");
|
||
|
||
const [loginForm, setLoginForm] = useState({
|
||
username: "",
|
||
password: "",
|
||
});
|
||
|
||
const [registerForm, setRegisterForm] = useState({
|
||
phone: "",
|
||
fullName: "",
|
||
username: "",
|
||
password: "",
|
||
});
|
||
|
||
const usernamePattern = /^[A-Za-z0-9]*$/;
|
||
const usernameErrorMessage = "نام کاربری فقط باید با حروف انگلیسی و عدد وارد شود";
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.name === "username") {
|
||
if (!usernamePattern.test(e.target.value)) {
|
||
setRegisterUsernameError(usernameErrorMessage);
|
||
return;
|
||
}
|
||
setRegisterUsernameError("");
|
||
}
|
||
|
||
setRegisterForm({
|
||
...registerForm,
|
||
[e.target.name]: e.target.value,
|
||
});
|
||
};
|
||
|
||
const handleRegister = async () => {
|
||
setRegisterError("");
|
||
|
||
const phone = registerForm.phone.trim();
|
||
const username = registerForm.username.trim();
|
||
const password = registerForm.password.trim();
|
||
const fullName = registerForm.fullName.trim();
|
||
|
||
if (!usernamePattern.test(username)) {
|
||
setRegisterUsernameError(usernameErrorMessage);
|
||
return;
|
||
}
|
||
|
||
if (!phone || !username || !password || !fullName) {
|
||
setRegisterError("لطفاً فیلدها را پر کنید");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const payload = { phone, username, password, fullName };
|
||
const res = await registerUser(payload);
|
||
const role = res.data?.user?.role;
|
||
const displayName = res.data?.user?.fullName || fullName || username;
|
||
|
||
localStorage.setItem("accessToken", res.data.accessToken);
|
||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
||
localStorage.setItem("username", username);
|
||
localStorage.setItem("fullName", displayName);
|
||
localStorage.setItem("role", role.toLowerCase());
|
||
|
||
const userData = { username, displayName };
|
||
|
||
onClose(); // بستن مودال
|
||
if (onRegisterSuccess) {
|
||
onRegisterSuccess(userData);
|
||
}
|
||
|
||
} catch (error: any) {
|
||
const message = error?.message?.toLowerCase() || "";
|
||
const usernameDuplicate = message.includes("username");
|
||
const phoneDuplicate = message.includes("phone");
|
||
|
||
if (usernameDuplicate && phoneDuplicate) {
|
||
setRegisterError("نام کاربری و شماره موبایل قبلاً ثبت شدهاند");
|
||
} else if (usernameDuplicate) {
|
||
setRegisterError("این نام کاربری قبلاً ثبت شده است");
|
||
} else if (phoneDuplicate) {
|
||
setRegisterError("این شماره موبایل قبلاً ثبت شده است");
|
||
} else {
|
||
setRegisterError("خطا در ثبت نام");
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleLogin = async () => {
|
||
setLoginError("");
|
||
|
||
const username = loginForm.username.trim();
|
||
const password = loginForm.password.trim();
|
||
|
||
if (!usernamePattern.test(username)) {
|
||
setLoginUsernameError(usernameErrorMessage);
|
||
return;
|
||
}
|
||
|
||
if (!username || !password) {
|
||
setLoginError("لطفاً نام کاربری و رمز عبور را وارد کنید");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await loginUser({ username, password });
|
||
const role = res.data?.user?.role;
|
||
const displayName = res.data?.user?.fullName || username;
|
||
|
||
localStorage.setItem("role", role.toLowerCase());
|
||
localStorage.setItem("accessToken", res.data.accessToken);
|
||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
||
localStorage.setItem("username", username);
|
||
localStorage.setItem("fullName", displayName);
|
||
|
||
const userData = { username, displayName };
|
||
|
||
onClose(); // بستن مودال
|
||
if (onLoginSuccess) {
|
||
onLoginSuccess(userData);
|
||
}
|
||
|
||
window.location.href="/dashboard?success=login";
|
||
|
||
} catch (error: any) {
|
||
const msg = error?.message?.toLowerCase() || "";
|
||
if (
|
||
msg.includes("not found") ||
|
||
(msg.includes("user") && msg.includes("not")) ||
|
||
(msg.includes("username") && msg.includes("not"))
|
||
) {
|
||
setLoginError("این نام کاربری وجود ندارد");
|
||
return;
|
||
}
|
||
|
||
if (
|
||
msg.includes("password") ||
|
||
msg.includes("invalid") ||
|
||
msg.includes("incorrect")
|
||
) {
|
||
setLoginError("رمز عبور اشتباه است");
|
||
return;
|
||
}
|
||
|
||
setLoginError("خطا در ورود. لطفاً دوباره تلاش کنید.");
|
||
}
|
||
};
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[#1A2332]/40 backdrop-blur-sm transition-opacity" dir="rtl">
|
||
<div
|
||
className="absolute inset-0"
|
||
onClick={onClose}
|
||
></div>
|
||
|
||
<div className="relative w-full max-w-sm bg-white rounded-3xl shadow-2xl p-8">
|
||
{/* دکمه بستن */}
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute cursor-pointer top-4 left-5 text-gray-400 hover:text-gray-700 transition"
|
||
>
|
||
✕
|
||
</button>
|
||
|
||
<div className="flex flex-col items-center mt-2 w-full">
|
||
{/* Tabs */}
|
||
<div className="flex w-full bg-gray-100 rounded-xl p-1 mb-6">
|
||
<button
|
||
onClick={() => setActiveTab("login")}
|
||
className={`flex-1 cursor-pointer py-2 text-sm rounded-lg transition ${activeTab === "login"
|
||
? "bg-white shadow text-[#1A2332] font-semibold"
|
||
: "text-gray-500"
|
||
}`}
|
||
>
|
||
ورود
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("register")}
|
||
className={`flex-1 cursor-pointer py-2 text-sm rounded-lg transition ${activeTab === "register"
|
||
? "bg-white shadow text-[#1A2332] font-semibold"
|
||
: "text-gray-500"
|
||
}`}
|
||
>
|
||
ثبت نام
|
||
</button>
|
||
</div>
|
||
|
||
{/* ---------------- LOGIN ---------------- */}
|
||
{activeTab === "login" && (
|
||
<div className="w-full space-y-5">
|
||
<div className="rounded-2xl border border-[#ffde7a] bg-[linear-gradient(135deg,#fff8dd_0%,#fffdf6_100%)] px-4 py-4 text-right">
|
||
<p className="text-base font-semibold text-[#1A2332]">ورود سریع به حساب کاربری</p>
|
||
<p className="mt-1 text-xs leading-6 text-gray-600">
|
||
سفارشها، وضعیت خرید و اطلاعات حسابتان را یکجا و بدون دردسر مدیریت کنید.
|
||
</p>
|
||
</div>
|
||
|
||
{formType === "mobile" && (
|
||
<div className="w-full space-y-4">
|
||
<FloatingInput
|
||
value={loginMobile}
|
||
onChange={(e) => setLoginMobile(e.target.value)}
|
||
type="tel"
|
||
dir="ltr"
|
||
inputMode="tel"
|
||
autoComplete="tel"
|
||
label="شماره موبایل"
|
||
className="text-left tracking-[0.18em]"
|
||
leftSlot={<IranPhoneAdornment />}
|
||
/>
|
||
|
||
<button className="w-full cursor-pointer rounded-2xl bg-[#ffb900] py-3.5 text-sm font-semibold text-[#1A2332] hover:bg-[#e5a600]">
|
||
ورود
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setFormType("password")}
|
||
className="w-full cursor-pointer text-xs text-gray-500 hover:text-[#1A2332]"
|
||
>
|
||
ورود با نام کاربری و رمز عبور
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{formType === "password" && (
|
||
<div className="w-full space-y-4">
|
||
<p className="text-xs leading-6 text-gray-500">
|
||
خوش برگشتی. برای ادامه، نام کاربری و رمز عبورت را وارد کن.
|
||
</p>
|
||
|
||
<FloatingInput
|
||
value={loginForm.username}
|
||
onChange={(e) => {
|
||
const username = e.target.value;
|
||
if (!usernamePattern.test(username)) {
|
||
setLoginUsernameError(usernameErrorMessage);
|
||
return;
|
||
}
|
||
setLoginUsernameError("");
|
||
setLoginForm((prev) => ({
|
||
...prev,
|
||
username,
|
||
}));
|
||
}}
|
||
type="text"
|
||
dir="ltr"
|
||
autoComplete="username"
|
||
label="نام کاربری"
|
||
className="text-left"
|
||
/>
|
||
{loginUsernameError && (
|
||
<div className="mt-1 text-right text-xs text-red-500">
|
||
{loginUsernameError}
|
||
</div>
|
||
)}
|
||
|
||
<FloatingInput
|
||
value={loginForm.password}
|
||
onChange={(e) => {
|
||
const password = e.target.value;
|
||
setLoginForm((prev) => ({
|
||
...prev,
|
||
password,
|
||
}));
|
||
if (!password) {
|
||
setShowLoginPassword(false);
|
||
}
|
||
}}
|
||
type={showLoginPassword ? "text" : "password"}
|
||
dir="ltr"
|
||
autoComplete="current-password"
|
||
label="رمز عبور"
|
||
className="text-left"
|
||
rightSlot={loginForm.password ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowLoginPassword(!showLoginPassword)}
|
||
className="cursor-pointer p-1 text-gray-500 hover:text-gray-700"
|
||
>
|
||
{showLoginPassword ? (
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M3 3l18 18M10.477 10.477A3 3 0 0113.5 13.5m-7.09-2.664A9.956 9.956 0 003 12s2.91-6 9-6a9.953 9.953 0 016.328 2.318M15.54 15.54A9.953 9.953 0 0112 18c-6.09 0-9-6-9-6a9.956 9.956 0 012.41-3.868" />
|
||
</svg>
|
||
) : (
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
) : undefined}
|
||
/>
|
||
|
||
<button onClick={handleLogin} className="w-full cursor-pointer rounded-2xl bg-[#ffb900] py-3.5 text-sm font-semibold text-[#1A2332] hover:bg-[#e5a600]">
|
||
ورود
|
||
</button>
|
||
{loginError && (
|
||
<div className="mt-2 text-center text-sm text-red-500">
|
||
{loginError}
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => setFormType("mobile")}
|
||
className="w-full cursor-pointer text-xs text-gray-500 hover:text-[#1A2332]"
|
||
>
|
||
ورود با شماره موبایل
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ---------------- REGISTER ---------------- */}
|
||
{activeTab === "register" && (
|
||
<div className="w-full space-y-4">
|
||
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-4 text-right">
|
||
<p className="text-base font-semibold text-[#1A2332]">چند قدم تا شروع خرید هوشمند</p>
|
||
<p className="mt-1 text-xs leading-6 text-gray-600">
|
||
با ساخت حساب، پیگیری سفارشها و دسترسی سریع به سبد خرید همیشه همراهت میماند.
|
||
</p>
|
||
</div>
|
||
|
||
<FloatingInput
|
||
name="phone"
|
||
value={registerForm.phone}
|
||
onChange={handleChange}
|
||
type="tel"
|
||
dir="ltr"
|
||
inputMode="tel"
|
||
autoComplete="tel"
|
||
label="شماره موبایل"
|
||
className="text-left tracking-[0.18em]"
|
||
leftSlot={<IranPhoneAdornment />}
|
||
/>
|
||
|
||
<FloatingInput
|
||
name="fullName"
|
||
value={registerForm.fullName}
|
||
onChange={handleChange}
|
||
type="text"
|
||
autoComplete="name"
|
||
label="نام و نام خانوادگی"
|
||
/>
|
||
|
||
<FloatingInput
|
||
name="username"
|
||
value={registerForm.username}
|
||
onChange={handleChange}
|
||
type="text"
|
||
dir="ltr"
|
||
autoComplete="username"
|
||
label="نام کاربری"
|
||
className="text-left"
|
||
/>
|
||
{registerUsernameError && (
|
||
<div className="mt-1 text-right text-xs text-red-500">
|
||
{registerUsernameError}
|
||
</div>
|
||
)}
|
||
|
||
<FloatingInput
|
||
name="password"
|
||
value={registerForm.password}
|
||
onChange={(e) => {
|
||
handleChange(e);
|
||
if (!e.target.value) {
|
||
setShowPassword(false);
|
||
}
|
||
}}
|
||
type={showPassword ? "text" : "password"}
|
||
dir="ltr"
|
||
autoComplete="new-password"
|
||
label="رمز عبور"
|
||
className="text-left"
|
||
rightSlot={registerForm.password ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
className="cursor-pointer text-gray-500 hover:text-gray-700"
|
||
>
|
||
{showPassword ? (
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M3 3l18 18M10.477 10.477A3 3 0 0113.5 13.5m-7.09-2.664A9.956 9.956 0 003 12s2.91-6 9-6a9.953 9.953 0 016.328 2.318M15.54 15.54A9.953 9.953 0 0112 18c-6.09 0-9-6-9-6a9.956 9.956 0 012.41-3.868" />
|
||
</svg>
|
||
) : (
|
||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none"
|
||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round"
|
||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
) : undefined}
|
||
/>
|
||
|
||
<p className="text-xs leading-6 text-gray-500">
|
||
با ثبتنام، تجربه خرید سریعتر و دسترسی راحتتر به حساب کاربری برایت فعال میشود.
|
||
</p>
|
||
|
||
<button
|
||
onClick={handleRegister}
|
||
className="w-full cursor-pointer rounded-2xl bg-[#ffb900] py-3.5 text-sm font-semibold text-[#1A2332] hover:bg-[#e5a600]"
|
||
>
|
||
ثبت نام
|
||
</button>
|
||
|
||
{registerError && (
|
||
<div className="mt-2 text-center text-sm text-red-500">
|
||
{registerError}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|