Files
parsshop/components/Auth.tsx

528 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}