Auth Modal / All Cart API's Done and Local storage Logic handle / Add cart Control button
This commit is contained in:
527
components/Auth.tsx
Normal file
527
components/Auth.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
105
components/CartControls.tsx
Normal file
105
components/CartControls.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
import { Plus, Minus, Trash2, ShoppingCart } from "lucide-react";
|
||||
import { useCart } from "./context/cartcontext"; // مسیر را تنظیم کنید
|
||||
import { addToCartApi, updateCartItemQuantityApi, removeCartItemApi } from "@/public/src/services/cart/api"; // مسیر را تنظیم کنید
|
||||
import { Product } from "@/public/src/types/product";
|
||||
|
||||
interface CartControlsProps {
|
||||
product: Product;
|
||||
// اگر کاربر لاگین باشد و محصول در سبد باشد، این آیدی باید پاس داده شود
|
||||
cartItemId?: string | null;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export default function CartControls({ product, cartItemId, quantity }: CartControlsProps) {
|
||||
const { addToCart, decreaseQuantity } = useCart();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isLoggedIn = typeof window !== 'undefined' ? !!localStorage.getItem('refreshToken') : false;
|
||||
|
||||
// هندلر افزایش یا افزودن جدید
|
||||
const handleIncrease = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isLoggedIn) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (quantity === 0) {
|
||||
// افزودن محصول جدید به سبد سرور
|
||||
await addToCartApi(product.id, 1);
|
||||
} else if (cartItemId) {
|
||||
// افزایش تعداد محصول موجود در سبد سرور
|
||||
await updateCartItemQuantityApi(cartItemId, quantity + 1);
|
||||
}
|
||||
window.dispatchEvent(new Event('cartUpdated'));
|
||||
} catch (error) {
|
||||
console.error("خطا در ارتباط با سرور:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
// کاربر مهمان
|
||||
addToCart(product);
|
||||
}
|
||||
};
|
||||
|
||||
// هندلر کاهش یا حذف
|
||||
const handleDecrease = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isLoggedIn && cartItemId) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (quantity <= 1) {
|
||||
await removeCartItemApi(cartItemId);
|
||||
} else {
|
||||
await updateCartItemQuantityApi(cartItemId, quantity - 1);
|
||||
}
|
||||
window.dispatchEvent(new Event('cartUpdated'));
|
||||
} catch (error) {
|
||||
console.error("خطا در کاهش محصول:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
// کاربر مهمان
|
||||
decreaseQuantity(product.id);
|
||||
}
|
||||
};
|
||||
|
||||
if (quantity > 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg py-1 px-2 shadow-sm"
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
>
|
||||
<button onClick={handleIncrease} disabled={isLoading} className="cursor-pointer py-[3px] text-gray-500 hover:text-green-600 disabled:opacity-50 transition-colors">
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
|
||||
<span className="text-xs font-bold text-gray-800 w-3 text-center">
|
||||
{isLoading ? "..." : quantity}
|
||||
</span>
|
||||
|
||||
<button onClick={handleDecrease} disabled={isLoading} className="cursor-pointer py-[3px] text-gray-500 hover:text-red-500 disabled:opacity-50 transition-colors">
|
||||
{quantity <= 1 ? <Trash2 size={16} /> : <Minus size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleIncrease}
|
||||
disabled={isLoading}
|
||||
className="rounded-lg border border-[#e6e6e6] bg-gray-50 flex items-center p-2 text-gray-600 hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
||||
title="افزودن به سبد خرید"
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import { Home, ShieldX, ArrowRight } from 'lucide-react';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from 'react';
|
||||
import AuthModal from '@/components/Auth';
|
||||
|
||||
|
||||
|
||||
// تعریف تایپ برای پراپها (ورودیهای کامپوننت)
|
||||
interface NotLoginProps {
|
||||
@@ -10,12 +14,15 @@ interface NotLoginProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NotLogin({
|
||||
export default function NotLogin({
|
||||
buttonText = "بازگشت به صفحه اصلی", // مقدار پیشفرض
|
||||
returnPath = "/", // مقدار پیشفرض
|
||||
onClose
|
||||
onClose
|
||||
}: NotLoginProps) {
|
||||
const router = useRouter();
|
||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||
const [user, setUser] = useState<{ username: string; displayName: string } | null>(null);
|
||||
const [showRegisterSuccessDialog, setShowRegisterSuccessDialog] = useState(false);
|
||||
|
||||
// مدیریت عملیات کلیک روی دکمه
|
||||
const handleAction = () => {
|
||||
@@ -44,14 +51,35 @@ export default function NotLogin({
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">دسترسی غیرمجاز</h1>
|
||||
<p className="text-gray-500 text-sm leading-relaxed mb-6">برای ادامه مراحل باید ابتدا وارد حساب کاربری خود شوید.</p>
|
||||
|
||||
<button
|
||||
onClick={handleAction}
|
||||
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="flex mx-auto cursor-pointer items-center gap-2 bg-gray-900 text-white px-5 py-2.5 rounded-lg text-sm hover:bg-black transition"
|
||||
>
|
||||
{renderIcon()}
|
||||
{buttonText}
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<button onClick={() => setIsAuthModalOpen(true)}>
|
||||
ورود / ثبتنام
|
||||
</button>
|
||||
{/* فراخوانی مودال احراز هویت */}
|
||||
<AuthModal
|
||||
isOpen={isAuthModalOpen}
|
||||
onClose={() => setIsAuthModalOpen(false)}
|
||||
onLoginSuccess={(userData) => setUser(userData)}
|
||||
onRegisterSuccess={(userData) => {
|
||||
setUser(userData);
|
||||
setShowRegisterSuccessDialog(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,36 +2,105 @@
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Product } from '@/public/src/types/product';
|
||||
|
||||
import { getCartApi } from '@/public/src/services/cart/api'; // حتما مسیر ایمپورت api سبد خرید را چک کنید
|
||||
|
||||
export interface CartItem extends Product {
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
// تعریف تایپ پایه برای آیتمهای سرور (در صورت نیاز میتوانید فیلدهای بیشتری اضافه کنید)
|
||||
export interface ServerCartItem {
|
||||
id: string; // شناسه ردیف سبد خرید
|
||||
product: Product; // اطلاعات محصول
|
||||
quantity: number;
|
||||
unitPrice?: number;
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
interface CartContextType {
|
||||
// --- استیتهای لوکال (مهمان) ---
|
||||
cart: CartItem[];
|
||||
addToCart: (product: Product) => void;
|
||||
removeFromCart: (id: string) => void;
|
||||
decreaseQuantity: (id: string) => void;
|
||||
clearCart: () => void;
|
||||
|
||||
// --- استیتهای سرور (لاگین شده) ---
|
||||
serverCartItems: ServerCartItem[];
|
||||
serverSummary: any; // میتوانید تایپ دقیقتری برای summary بنویسید
|
||||
refreshServerCart: () => Promise<void>;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
|
||||
export function CartProvider({ children }: { children: ReactNode }) {
|
||||
// استیتهای لوکال
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
|
||||
// استیتهای سرور
|
||||
const [serverCartItems, setServerCartItems] = useState<ServerCartItem[]>([]);
|
||||
const [serverSummary, setServerSummary] = useState<any>(null);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
// 1. لود کردن سبد خرید لوکال در ابتدای کار
|
||||
useEffect(() => {
|
||||
const savedCart = localStorage.getItem('cart');
|
||||
if (savedCart) {
|
||||
setCart(JSON.parse(savedCart));
|
||||
try {
|
||||
setCart(JSON.parse(savedCart));
|
||||
} catch (e) {
|
||||
console.error("خطا در پارس کردن سبد خرید لوکال");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 2. ذخیره سبد خرید لوکال با هر بار تغییر
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cart', JSON.stringify(cart));
|
||||
}, [cart]);
|
||||
|
||||
// 3. تابع دریافت سبد خرید از سرور
|
||||
const refreshServerCart = async () => {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||||
if (token) {
|
||||
setIsLoggedIn(true);
|
||||
try {
|
||||
const data = await getCartApi();
|
||||
if (data) {
|
||||
setServerCartItems(data.items || []);
|
||||
setServerSummary(data.summary || null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("خطا در دریافت سبد خرید از سرور:", error);
|
||||
}
|
||||
} else {
|
||||
setIsLoggedIn(false);
|
||||
setServerCartItems([]);
|
||||
setServerSummary(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 4. گوش دادن به رویداد cartUpdated برای رفرش کردن سبد سرور
|
||||
useEffect(() => {
|
||||
// دریافت اولیه در زمان لود صفحه
|
||||
refreshServerCart();
|
||||
|
||||
// تعریف لیسنر برای رویداد اختصاصی
|
||||
const handleCartUpdate = () => {
|
||||
refreshServerCart();
|
||||
};
|
||||
|
||||
// اضافه کردن لیسنر
|
||||
window.addEventListener('cartUpdated', handleCartUpdate);
|
||||
|
||||
// پاکسازی لیسنر
|
||||
return () => {
|
||||
window.removeEventListener('cartUpdated', handleCartUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// --- توابع مدیریت سبد خرید لوکال ---
|
||||
const addToCart = (product: Product) => {
|
||||
setCart((prevCart) => {
|
||||
const existingItem = prevCart.find((item) => item.id === product.id);
|
||||
@@ -63,7 +132,22 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
||||
const clearCart = () => setCart([]);
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={{ cart, addToCart, removeFromCart, decreaseQuantity, clearCart }}>
|
||||
<CartContext.Provider
|
||||
value={{
|
||||
// لوکال
|
||||
cart,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
decreaseQuantity,
|
||||
clearCart,
|
||||
|
||||
// سرور
|
||||
serverCartItems,
|
||||
serverSummary,
|
||||
refreshServerCart,
|
||||
isLoggedIn
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { loginUser } from '@/public/src/services/auth/api';
|
||||
import { logoutUser } from '@/public/src/services/auth/api';
|
||||
import { useCategories } from './context/categoryprovider';
|
||||
import { getCartApi } from '@/public/src/services/cart/api';
|
||||
import AuthModal from './Auth';
|
||||
|
||||
const topBarLinks = [
|
||||
{ label: "بخش صنعتی", href: "/" },
|
||||
@@ -109,6 +110,7 @@ const dashboardMenuItems = [
|
||||
|
||||
export function Header() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { cart, removeFromCart } = useCart();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -131,8 +133,8 @@ export function Header() {
|
||||
const [loginMobile, setLoginMobile] = useState("");
|
||||
const [showRegisterSuccessDialog, setShowRegisterSuccessDialog] = useState(false);
|
||||
const [isOptimistic, setIsOptimistic] = useState(false);
|
||||
// ۱. افکت اول: وقتی کانتکست لوکال (cart) با کلیک کاربر آپدیت میشود،
|
||||
// منو را به حالت Optimistic میبریم تا تغییرات را درجا نشان دهد.
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setIsOptimistic(true);
|
||||
// بعد از ۱.۵ ثانیه (زمانی که قاعدتاً API سرور کارش تمام شده) به حالت عادی برمیگردد
|
||||
@@ -366,13 +368,10 @@ export function Header() {
|
||||
} finally {
|
||||
clearAuthState();
|
||||
setUserMenuOpen(false);
|
||||
router.push("/");
|
||||
window.location.href = "/";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const parsePrice = (priceStr?: number | null) => {
|
||||
if (!priceStr) return 0;
|
||||
return Number(priceStr.toString().replace(/,/g, ''));
|
||||
@@ -420,41 +419,6 @@ export function Header() {
|
||||
const [serverCartItems, setServerCartItems] = useState<any[]>([]);
|
||||
const [serverSummary, setServerSummary] = useState<any>(null);
|
||||
|
||||
// // درون کامپوننت هدر شما
|
||||
// useEffect(() => {
|
||||
// const fetchServerData = async () => {
|
||||
// const token = localStorage.getItem('accessToken');
|
||||
// if (token) {
|
||||
// setIsLoggedIn(true);
|
||||
// try {
|
||||
// const data = await getCartApi();
|
||||
// if (data) {
|
||||
// setServerCartItems(data.items || []);
|
||||
// setServerSummary(data.summary || null);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("خطا در همگامسازی سبد خرید سرور:", error);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// // --- این تابع جدید را اضافه کنید ---
|
||||
// const handleCartClear = () => {
|
||||
// setServerCartItems([]); // درجا لیست منو را خالی میکند
|
||||
// setServerSummary(null);
|
||||
// };
|
||||
|
||||
// // گوش دادن به رویدادها
|
||||
// window.addEventListener('cartUpdated', fetchServerData);
|
||||
// window.addEventListener('cartCleared', handleCartClear); // لیسنر جدید
|
||||
|
||||
// fetchServerData();
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener('cartUpdated', fetchServerData);
|
||||
// window.removeEventListener('cartCleared', handleCartClear); // کلینآپ لیسنر جدید
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
|
||||
|
||||
@@ -478,15 +442,15 @@ export function Header() {
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
};
|
||||
// --- این تابع جدید را اضافه کنید ---
|
||||
const handleCartClear = () => {
|
||||
setServerCartItems([]); // درجا لیست منو را خالی میکند
|
||||
setServerSummary(null);
|
||||
};
|
||||
// گوش دادن به رویدادها
|
||||
window.addEventListener('cartUpdated', fetchServerCart);
|
||||
window.addEventListener('cartCleared', handleCartClear); // لیسنر جدید
|
||||
|
||||
// --- این تابع جدید را اضافه کنید ---
|
||||
const handleCartClear = () => {
|
||||
setServerCartItems([]); // درجا لیست منو را خالی میکند
|
||||
setServerSummary(null);
|
||||
};
|
||||
// گوش دادن به رویدادها
|
||||
window.addEventListener('cartUpdated', fetchServerCart);
|
||||
window.addEventListener('cartCleared', handleCartClear); // لیسنر جدید
|
||||
|
||||
fetchServerCart();
|
||||
return () => {
|
||||
window.removeEventListener('cartUpdated', fetchServerCart);
|
||||
@@ -495,32 +459,6 @@ export function Header() {
|
||||
// در صورت نیاز به آپدیت شدن دراپداون با هر تغییر، میتوانید این تابع را به یک Event یا Context متصل کنید
|
||||
}, []);
|
||||
|
||||
// --- متغیرهای هوشمند برای جایگزینی در UI ---
|
||||
// const displayCart = isLoggedIn
|
||||
// ? serverCartItems.map(item => ({
|
||||
// id: item.product?.id || item.productId,
|
||||
// title: item.product?.title || "بدون نام",
|
||||
// brand: item.product?.brand || "متفرقه",
|
||||
// price: item.unitPrice || item.product?.price || 0,
|
||||
// quantity: item.quantity || 1,
|
||||
// image: item.product?.mainImageUrl || item.product?.image || "/placeholder.png"
|
||||
// }))
|
||||
// : cart; // cart از useCart() میآید
|
||||
|
||||
// const displayTotalQuantity = isLoggedIn && serverSummary
|
||||
// ? serverSummary.totalQuantity || serverSummary.itemsCount || 0
|
||||
// : cart.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
// const displayTotalPrice = isLoggedIn && serverSummary
|
||||
// ? serverSummary.totalPrice || serverSummary.total || 0
|
||||
// : totalPrice;
|
||||
|
||||
|
||||
|
||||
|
||||
// --- متغیرهای هوشمند اصلاح شده ---
|
||||
// اگر لاگین باشیم و در لحظهی کلیک (Optimistic) نباشیم، دیتای سرور را نشان میدهد
|
||||
// در غیر این صورت (برای نمایش آنی) دیتای لوکال را نشان میدهد.
|
||||
const displayCart = (isLoggedIn && !isOptimistic)
|
||||
? serverCartItems.map(item => ({
|
||||
id: item.product?.id || item.productId,
|
||||
@@ -805,7 +743,8 @@ export function Header() {
|
||||
|
||||
{!user ? (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
// onClick={() => setIsOpen(true)}
|
||||
onClick={() => setIsAuthModalOpen(true)}
|
||||
className="flex cursor-pointer items-center gap-2 px-3 py-2.5 bg-white border border-gray-300/60 rounded-xl text-xs text-gray-700 hover:bg-gray-50 transition"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4 text-gray-500">
|
||||
@@ -867,6 +806,17 @@ export function Header() {
|
||||
)}
|
||||
|
||||
|
||||
{/* فراخوانی مودال احراز هویت */}
|
||||
<AuthModal
|
||||
isOpen={isAuthModalOpen}
|
||||
onClose={() => setIsAuthModalOpen(false)}
|
||||
onLoginSuccess={(userData) => setUser(userData)}
|
||||
onRegisterSuccess={(userData) => {
|
||||
setUser(userData);
|
||||
setShowRegisterSuccessDialog(true);
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[#1A2332]/40 backdrop-blur-sm transition-opacity" dir="rtl">
|
||||
|
||||
@@ -877,7 +827,6 @@ export function Header() {
|
||||
|
||||
<div className="relative w-full max-w-sm bg-white rounded-3xl shadow-2xl p-8">
|
||||
|
||||
{/* دکمه بستن */}
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="absolute cursor-pointer top-4 left-5 text-gray-400 hover:text-gray-700 transition"
|
||||
@@ -887,7 +836,6 @@ export function Header() {
|
||||
|
||||
<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")}
|
||||
@@ -911,7 +859,6 @@ export function Header() {
|
||||
|
||||
</div>
|
||||
|
||||
{/* ---------------- LOGIN ---------------- */}
|
||||
|
||||
{activeTab === "login" && (
|
||||
<div className="w-full space-y-5">
|
||||
@@ -1046,7 +993,6 @@ export function Header() {
|
||||
{false && activeTab === "login" && (
|
||||
<div className="w-full">
|
||||
|
||||
{/* فرم موبایل */}
|
||||
{formType === "mobile" && (
|
||||
<div className="w-full space-y-4">
|
||||
|
||||
@@ -1070,7 +1016,6 @@ export function Header() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* فرم یوزر پس */}
|
||||
{formType === "password" && (
|
||||
<div className="w-full space-y-4">
|
||||
|
||||
@@ -1108,14 +1053,12 @@ export function Header() {
|
||||
className="absolute cursor-pointer left-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 p-1" // پدینگ اضافه شد تا کلیک راحتتر باشد
|
||||
>
|
||||
{showLoginPassword ? (
|
||||
// آیکون چشم خط خورده (eye-off)
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-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>
|
||||
) : (
|
||||
// آیکون چشم باز (eye)
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
@@ -1149,7 +1092,6 @@ export function Header() {
|
||||
</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">
|
||||
@@ -1298,14 +1240,12 @@ export function Header() {
|
||||
className="absolute cursor-pointer left-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPassword ? (
|
||||
// eye-off icon
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-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>
|
||||
) : (
|
||||
// eye icon
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
@@ -1317,16 +1257,6 @@ export function Header() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* <input
|
||||
name="confirmPassword"
|
||||
value={registerForm.confirmPassword}
|
||||
onChange={handleChange}
|
||||
type="password"
|
||||
placeholder="تکرار رمز عبور"
|
||||
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
|
||||
/> */}
|
||||
|
||||
<button
|
||||
onClick={handleRegister}
|
||||
className="w-full cursor-pointer py-3.5 bg-[#ffb900] hover:bg-[#e5a600] text-[#1A2332] font-semibold rounded-2xl text-sm"
|
||||
@@ -1347,7 +1277,7 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{showRegisterSuccessDialog && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-[#1A2332]/45 p-4 backdrop-blur-sm" dir="rtl">
|
||||
|
||||
@@ -1,69 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { Phone, Plus, ShoppingCart, Minus, Trash2 } from "lucide-react";
|
||||
import { Phone } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from 'next/link';
|
||||
import { useCart } from "./context/cartcontext";
|
||||
import { Product } from "@/public/src/types/product";
|
||||
import { addToCartApi } from "@/public/src/services/cart/api";
|
||||
import CartControls from "./CartControls";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product }: ProductCardProps) {
|
||||
const { addToCart, decreaseQuantity, cart } = useCart();
|
||||
// دریافت استیتهای لوکال و سرور از کانتکست
|
||||
const { cart, serverCartItems, isLoggedIn } = useCart();
|
||||
|
||||
const cartItem = cart.find(item => item.id === product.id);
|
||||
const quantity = cartItem ? cartItem.quantity : 0;
|
||||
const slug = product.slug;
|
||||
|
||||
// منطق یافتن شناسه ردیف سبد خرید و تعداد بر اساس وضعیت لاگین
|
||||
let cartItemId = null;
|
||||
let quantity = 0;
|
||||
|
||||
const handleIncrease = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// ۱. آپدیت آنی سبد خرید محلی (این کار باعث آپدیت درجا در UI میشود)
|
||||
addToCart(product);
|
||||
|
||||
// ۲. ارسال درخواست به سرور در پسزمینه
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
try {
|
||||
await addToCartApi(product.id, 1);
|
||||
// ارسال سیگنال به هدر (منو) برای دریافت اطلاعات جدید سرور بدون رفرش
|
||||
window.dispatchEvent(new Event('cartUpdated'));
|
||||
} catch (error) {
|
||||
console.error("خطا در افزودن محصول به سبد خرید سرور:", error);
|
||||
}
|
||||
if (isLoggedIn && serverCartItems) {
|
||||
// اگر لاگین بود، از سبد خرید سرور میخوانیم
|
||||
const serverItem = serverCartItems.find((item) => item.product?.id === product.id);
|
||||
if (serverItem) {
|
||||
cartItemId = serverItem.id;
|
||||
quantity = serverItem.quantity;
|
||||
}
|
||||
};
|
||||
|
||||
// --- هندلر کاهش / حذف از سبد خرید ---
|
||||
const handleDecrease = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
decreaseQuantity(product.id);
|
||||
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
try {
|
||||
// فرض بر این است که API مربوط به کاهش را در اینجا فراخوانی میکنید
|
||||
// await decreaseFromCartApi(product.id);
|
||||
|
||||
// پس از موفقیتآمیز بودن حذف/کاهش از سرور، دوباره سیگنال میدهیم
|
||||
window.dispatchEvent(new Event('cartUpdated'));
|
||||
} catch (error) {
|
||||
console.error("خطا در کاهش محصول از سبد خرید سرور:", error);
|
||||
}
|
||||
} else {
|
||||
// اگر مهمان بود، از لوکال استوریج میخوانیم
|
||||
const localItem = cart.find(item => item.id === product.id);
|
||||
if (localItem) {
|
||||
quantity = localItem.quantity;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formattedPrice = product.price
|
||||
? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')
|
||||
: null;
|
||||
|
||||
|
||||
return (
|
||||
<Link href={`/product/${slug}`} scroll={true} className="bg-white gap-1 flex flex-col justify-between border border-gray-200 rounded-xl shadow-sm p-4 transition-all hover:shadow-md">
|
||||
|
||||
@@ -107,41 +83,12 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
</span>
|
||||
|
||||
{product.stock ? (
|
||||
quantity > 0 ? (
|
||||
<div
|
||||
className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg py-1 px-2 shadow-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleIncrease}
|
||||
className="text-gray-500 hover:text-green-600 transition-colors"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
|
||||
<span className="text-xs font-bold text-gray-800 w-3 text-center">
|
||||
{quantity}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={handleDecrease}
|
||||
className="text-gray-500 hover:text-red-500 transition-colors"
|
||||
>
|
||||
{quantity === 1 ? <Trash2 size={16} /> : <Minus size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleIncrease}
|
||||
className="rounded-lg border border-[#e6e6e6] bg-gray-50 flex items-center p-2 text-gray-600 hover:bg-gray-200 transition-colors"
|
||||
title="افزودن به سبد خرید"
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
</button>
|
||||
)
|
||||
// استفاده از کامپوننت CartControls دقیقا با همان UI قبلی
|
||||
<CartControls
|
||||
product={product}
|
||||
cartItemId={cartItemId}
|
||||
quantity={quantity}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[10px] text-red-500 font-medium bg-red-50 px-2 py-1 rounded-md">
|
||||
عدم موجودی
|
||||
|
||||
Reference in New Issue
Block a user