Auth Modal / All Cart API's Done and Local storage Logic handle / Add cart Control button

This commit is contained in:
haniyeroozmand
2026-04-08 22:43:40 +03:30
parent 8be715b34b
commit 411109ee7e
8 changed files with 959 additions and 291 deletions

527
components/Auth.tsx Normal file
View 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
View 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>
);
}

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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">

View File

@@ -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">
عدم موجودی