diff --git a/app/cart/page.tsx b/app/cart/page.tsx index f5559ad..a12c44b 100644 --- a/app/cart/page.tsx +++ b/app/cart/page.tsx @@ -4,52 +4,27 @@ import { useCart } from "@/components/context/cartcontext"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState, useEffect } from "react"; // اضافه شدن useEffect +import { useState, useEffect, useRef } from "react"; import { Trash2, ShoppingBag, ChevronLeft, - Plus, - Minus, ShieldCheck, Truck, CreditCard } from "lucide-react"; import NotLogin from "@/components/Notlogin"; -import { getCartApi } from "@/public/src/services/cart/api"; // ایمپورت API -import { clearServerCartApi } from "@/public/src/services/cart/api"; +import { getCartApi, addToCartApi, clearServerCartApi } from "@/public/src/services/cart/api"; +import CartControls from "@/components/CartControls"; export default function CartPage() { - const { - cart, - clearCart, - addToCart, - decreaseQuantity, + const { + cart, + clearCart, } = useCart(); - // const handleClearAll = async () => { - // // ۱. پاک کردن استیت لوکال - // clearCart(); - - // const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; - // if (token) { - // try { - // // ۲. پاک کردن از سرور - // await clearServerCartApi(); - - // // ۳. آپدیت کردن صفحه برای دریافت دیتای جدید سرور (سبد خالی) - // router.refresh(); - - // } catch (error) { - // console.error(error); - // } - // } - // }; - - const router = useRouter(); - const [showNotLogin, setShowNotLogin] = useState(false); // --- استیت‌های مربوط به دریافت اطلاعات سرور --- @@ -58,35 +33,84 @@ export default function CartPage() { const [serverSummary, setServerSummary] = useState(null); const [isLoading, setIsLoading] = useState(true); - // --- منطق اصلاح شده دکمه حذف همه --- - const handleClearAll = async () => { - // ۱. پاک کردن استیت‌های لوکال و استیت‌های همین صفحه - clearCart(); - setServerCartItems([]); - setServerSummary(null); - - // ۲. ارسال دستور مستقیم به هدر برای خالی شدن درجا (بدون درنگ) - window.dispatchEvent(new Event('cartCleared')); - - // ۳. فراخوانی API برای پاک کردن دیتابیس در پس‌زمینه - const token = typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null; - if (token) { + const handleClearAll = async () => { + clearCart(); + setServerCartItems([]); + setServerSummary(null); + + const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; + + if (token) { + try { + await clearServerCartApi(); + window.dispatchEvent(new Event('cartUpdated')); + } catch (error) { + console.error("خطا در پاک کردن سبد خرید سرور:", error); + } + } + }; + + // ۱. ایجاد قفل برای جلوگیری از اجرای تکراری + const isSyncingRef = useRef(false); + const hasFetchedRef = useRef(false); + + // --- منطق ادغام (Merge) و دریافت اطلاعات سرور --- + useEffect(() => { + const fetchAndMergeCart = async () => { + const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; + + if (!token) { + setIsLoggedIn(false); + setIsLoading(false); + return; + } + + setIsLoggedIn(true); + + // ۲. بررسی قفل: اگر توکن داریم، سبد لوکال پر است و قبلا در حال سینک نبودیم + if (cart.length > 0 && !isSyncingRef.current) { + isSyncingRef.current = true; + setIsLoading(true); + try { - await clearServerCartApi(); // متد DELETE - // اینجا دیگر نیازی به cartUpdated نیست چون هدر را به صورت دستی خالی کردیم + await Promise.all( + cart.map(item => addToCartApi(item.id, item.quantity)) + ); + + clearCart(); + window.dispatchEvent(new Event('cartUpdated')); } catch (error) { - console.error("خطا در پاک کردن سبد خرید سرور:", error); + console.error("خطا در انتقال محصولات لوکال به سرور:", error); } } + + // ۳. دریافت اطلاعات از سرور + if (cart.length === 0 && !hasFetchedRef.current || isSyncingRef.current) { + try { + const data = await getCartApi(); + if (data) { + setServerCartItems(data.items || []); + setServerSummary(data.summary || null); + hasFetchedRef.current = true; + } + } catch (error) { + console.error("خطا در دریافت اطلاعات سبد خرید:", error); + } finally { + setIsLoading(false); + } + } else { + setIsLoading(false); + } }; - - - // --- useEffect برای بررسی لاگین و دریافت دیتای سرور --- + + fetchAndMergeCart(); + }, [cart.length, clearCart]); + + // --- لیسنر برای آپدیت لحظه‌ای وقتی CartControls مقادیر را تغییر می‌دهد --- useEffect(() => { - const fetchServerCart = async () => { - const token = localStorage.getItem('accessToken'); + const updateServerData = async () => { + const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; if (token) { - setIsLoggedIn(true); try { const data = await getCartApi(); if (data) { @@ -94,21 +118,20 @@ export default function CartPage() { setServerSummary(data.summary || null); } } catch (error) { - console.error("خطا در دریافت اطلاعات سبد خرید:", error); + console.error("خطا در آپدیت سبد خرید سرور:", error); } - } else { - setIsLoggedIn(false); } - setIsLoading(false); }; - fetchServerCart(); + // هر بار CartControls رویداد را فایر کرد، استیت‌های این صفحه رفرش می‌شوند + window.addEventListener('cartUpdated', updateServerData); + return () => { + window.removeEventListener('cartUpdated', updateServerData); + }; }, []); - - // بررسی لاگین کاربر const handleCheckoutNavigation = () => { - const token = localStorage.getItem('accessToken'); + const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; if (token) { router.push('/checkout'); } else { @@ -116,25 +139,34 @@ export default function CartPage() { } }; - // تبدیل رشته قیمت به عدد const parsePrice = (priceStr?: number | string | null) => { if (!priceStr) return 0; return Number(priceStr.toString().replace(/,/g, '')); }; - // --- منطق یکپارچه‌سازی: اگر لاگین بود دیتای سرور، در غیر این‌صورت دیتای لوکال --- + // آماده‌سازی لیست سبد خرید برای نمایش + اضافه کردن کلید product برای کامپوننت CartControls const displayCart = isLoggedIn ? serverCartItems.map(item => ({ - id: item.product?.id || item.productId, + id: item.id, + productId: item.product?.id || item.productId, + product: item.product, // <-- اضافه شد برای پاس دادن به CartControls 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.map(item => ({ + id: item.id, + productId: item.id, + product: item, // <-- در حالت لوکال، خود آیتم همان محصول است + title: item.title, + brand: item.brand, + price: item.price, + quantity: item.quantity, + image: item.image || "/placeholder.png" + })); - // محاسبه قیمت کل و تعداد کل const totalPrice = isLoggedIn && serverSummary ? serverSummary.totalPrice || serverSummary.total || 0 : cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0); @@ -143,12 +175,7 @@ export default function CartPage() { ? serverSummary.totalQuantity || serverSummary.itemsCount || 0 : cart.reduce((total, item) => total + item.quantity, 0); - // --- جلوگیری از پرش تصویر قبل از لود شدن دیتا --- - if (isLoading) { - return
در حال بارگذاری...
; - } - - // دیزاین حالت سبد خرید خالی (تغییر cart.length به displayCart.length) + // دیزاین حالت سبد خرید خالی if (displayCart.length === 0) { return (
@@ -234,7 +261,6 @@ export default function CartPage() {
- {/* حلقه مپ روی displayCart اعمال شده است */} {displayCart.map((item) => { const itemTotal = parsePrice(item.price) * item.quantity; return ( @@ -250,15 +276,12 @@ export default function CartPage() { {item.price ? `${itemTotal.toLocaleString('fa-IR')} ت` : 'استعلام'} -
- - {item.quantity} - -
+ +
); diff --git a/components/Auth.tsx b/components/Auth.tsx new file mode 100644 index 0000000..236a537 --- /dev/null +++ b/components/Auth.tsx @@ -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) => void; + label: string; + type?: string; + dir?: "rtl" | "ltr"; + inputMode?: React.HTMLAttributes["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 ( + + ); +} + +function IranPhoneAdornment() { + return ( +
+ 🇮🇷 + +98 +
+ ); +} +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) => { + 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 ( +
+
+ +
+ {/* دکمه بستن */} + + +
+ {/* Tabs */} +
+ + + +
+ + {/* ---------------- LOGIN ---------------- */} + {activeTab === "login" && ( +
+
+

ورود سریع به حساب کاربری

+

+ سفارش‌ها، وضعیت خرید و اطلاعات حساب‌تان را یک‌جا و بدون دردسر مدیریت کنید. +

+
+ + {formType === "mobile" && ( +
+ setLoginMobile(e.target.value)} + type="tel" + dir="ltr" + inputMode="tel" + autoComplete="tel" + label="شماره موبایل" + className="text-left tracking-[0.18em]" + leftSlot={} + /> + + + + +
+ )} + + {formType === "password" && ( +
+

+ خوش برگشتی. برای ادامه، نام کاربری و رمز عبورت را وارد کن. +

+ + { + 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 && ( +
+ {loginUsernameError} +
+ )} + + { + 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 ? ( + + ) : undefined} + /> + + + {loginError && ( +
+ {loginError} +
+ )} + + +
+ )} +
+ )} + + {/* ---------------- REGISTER ---------------- */} + {activeTab === "register" && ( +
+
+

چند قدم تا شروع خرید هوشمند

+

+ با ساخت حساب، پیگیری سفارش‌ها و دسترسی سریع به سبد خرید همیشه همراهت می‌ماند. +

+
+ + } + /> + + + + + {registerUsernameError && ( +
+ {registerUsernameError} +
+ )} + + { + handleChange(e); + if (!e.target.value) { + setShowPassword(false); + } + }} + type={showPassword ? "text" : "password"} + dir="ltr" + autoComplete="new-password" + label="رمز عبور" + className="text-left" + rightSlot={registerForm.password ? ( + + ) : undefined} + /> + +

+ با ثبت‌نام، تجربه خرید سریع‌تر و دسترسی راحت‌تر به حساب کاربری برایت فعال می‌شود. +

+ + + + {registerError && ( +
+ {registerError} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/components/CartControls.tsx b/components/CartControls.tsx new file mode 100644 index 0000000..d2b5955 --- /dev/null +++ b/components/CartControls.tsx @@ -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 ( +
{ e.preventDefault(); e.stopPropagation(); }} + > + + + + {isLoading ? "..." : quantity} + + + +
+ ); + } + + return ( + + ); +} diff --git a/components/Notlogin.tsx b/components/Notlogin.tsx index 68dcaf1..2be6e46 100644 --- a/components/Notlogin.tsx +++ b/components/Notlogin.tsx @@ -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({

دسترسی غیرمجاز

برای ادامه مراحل باید ابتدا وارد حساب کاربری خود شوید.

- - + + + + + + + {/* فراخوانی مودال احراز هویت */} + setIsAuthModalOpen(false)} + onLoginSuccess={(userData) => setUser(userData)} + onRegisterSuccess={(userData) => { + setUser(userData); + setShowRegisterSuccessDialog(true); + }} + /> + + + ); diff --git a/components/context/cartcontext.tsx b/components/context/cartcontext.tsx index 0213e57..a48f6cf 100644 --- a/components/context/cartcontext.tsx +++ b/components/context/cartcontext.tsx @@ -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; + isLoggedIn: boolean; } const CartContext = createContext(undefined); export function CartProvider({ children }: { children: ReactNode }) { + // استیت‌های لوکال const [cart, setCart] = useState([]); + + // استیت‌های سرور + const [serverCartItems, setServerCartItems] = useState([]); + const [serverSummary, setServerSummary] = useState(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 ( - + {children} ); diff --git a/components/header.tsx b/components/header.tsx index 54f1c6a..8b9be09 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -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([]); const [serverSummary, setServerSummary] = useState(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 ? ( - - - {quantity} - - - - - ) : ( - - ) + // استفاده از کامپوننت CartControls دقیقا با همان UI قبلی + ) : ( عدم موجودی diff --git a/public/src/services/cart/api.tsx b/public/src/services/cart/api.tsx index 7160763..6653646 100644 --- a/public/src/services/cart/api.tsx +++ b/public/src/services/cart/api.tsx @@ -58,30 +58,6 @@ export async function addToCartApi(productId: string, quantity: number = 1) { } } - -// cartApi.js -export const updateCartItemApi = async (itemId:any, quantity:any, token:any) => { - try { - const response = await fetch(`${API_BASE_URL}/users/me/cart/items/${itemId}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ quantity }) - }); - - if (!response.ok) { - throw new Error('خطا در به‌روزرسانی سبد خرید'); - } - - const data = await response.json(); - return data; // حاوی items و summary به‌روزشده - } catch (error) { - console.error("Error updating cart item:", error); - throw error; - } - }; export const clearServerCartApi = async () => { const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; @@ -101,3 +77,51 @@ export const updateCartItemApi = async (itemId:any, quantity:any, token:any) => return await response.json(); }; + +// آپدیت تعداد یک محصول در سبد خرید (PATCH) +export async function updateCartItemQuantityApi(itemId: string, quantity: number) { + try { + const response = await fetch(`${API_BASE_URL}/users/me/cart/items/${itemId}`, { + method: 'PATCH', + headers: getAuthHeaders(), + body: JSON.stringify({ quantity }) // ارسال تعداد نهایی + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("جزئیات خطای سرور (آپدیت تعداد):", response.status, errorText); + throw new Error(`خطا در آپدیت تعداد محصول: کد ${response.status}`); + } + + const data = await response.json(); + return data.data; // برگرداندن اطلاعات آپدیت شده سبد خرید + } catch (error) { + console.error(error); + throw error; + } +} + + +// حذف کامل یک محصول از سبد خرید (DELETE) +export async function removeCartItemApi(itemId: string) { + try { + const response = await fetch(`${API_BASE_URL}/users/me/cart/items/${itemId}`, { + method: 'DELETE', + headers: getAuthHeaders(), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("جزئیات خطای سرور (حذف آیتم):", response.status, errorText); + throw new Error(`خطا در حذف آیتم از سبد خرید: کد ${response.status}`); + } + + // در متد DELETE معمولا یا بدنه خالی برمی‌گردد یا پیغام موفقیت + // در صورت نیاز به خواندن json از سرور: + const data = await response.json().catch(() => ({})); + return data; + } catch (error) { + console.error(error); + throw error; + } +}