335 lines
18 KiB
TypeScript
335 lines
18 KiB
TypeScript
'use client';
|
||
|
||
import { useCart } from "@/components/context/cartcontext";
|
||
import Image from "next/image";
|
||
import Link from "next/link";
|
||
import { useRouter } from "next/navigation";
|
||
import { useState, useEffect, useRef } from "react";
|
||
import {
|
||
Trash2,
|
||
ShoppingBag,
|
||
ChevronLeft,
|
||
ShieldCheck,
|
||
Truck,
|
||
CreditCard
|
||
} from "lucide-react";
|
||
|
||
import NotLogin from "@/components/Notlogin";
|
||
import { getCartApi, addToCartApi, clearServerCartApi } from "@/public/src/services/cart/api";
|
||
import CartControls from "@/components/CartControls";
|
||
|
||
export default function CartPage() {
|
||
const {
|
||
cart,
|
||
clearCart,
|
||
} = useCart();
|
||
|
||
const router = useRouter();
|
||
const [showNotLogin, setShowNotLogin] = useState(false);
|
||
|
||
// --- استیتهای مربوط به دریافت اطلاعات سرور ---
|
||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||
const [serverCartItems, setServerCartItems] = useState<any[]>([]);
|
||
const [serverSummary, setServerSummary] = useState<any>(null);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
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 Promise.all(
|
||
cart.map(item => addToCartApi(item.id, item.quantity))
|
||
);
|
||
|
||
clearCart();
|
||
window.dispatchEvent(new Event('cartUpdated'));
|
||
} catch (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);
|
||
}
|
||
};
|
||
|
||
fetchAndMergeCart();
|
||
}, [cart.length, clearCart]);
|
||
|
||
// --- لیسنر برای آپدیت لحظهای وقتی CartControls مقادیر را تغییر میدهد ---
|
||
useEffect(() => {
|
||
const updateServerData = async () => {
|
||
const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||
if (token) {
|
||
try {
|
||
const data = await getCartApi();
|
||
if (data) {
|
||
setServerCartItems(data.items || []);
|
||
setServerSummary(data.summary || null);
|
||
}
|
||
} catch (error) {
|
||
console.error("خطا در آپدیت سبد خرید سرور:", error);
|
||
}
|
||
}
|
||
};
|
||
|
||
// هر بار CartControls رویداد را فایر کرد، استیتهای این صفحه رفرش میشوند
|
||
window.addEventListener('cartUpdated', updateServerData);
|
||
return () => {
|
||
window.removeEventListener('cartUpdated', updateServerData);
|
||
};
|
||
}, []);
|
||
|
||
const handleCheckoutNavigation = () => {
|
||
const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||
if (token) {
|
||
router.push('/checkout');
|
||
} else {
|
||
setShowNotLogin(true);
|
||
}
|
||
};
|
||
|
||
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.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.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);
|
||
|
||
const totalItems = isLoggedIn && serverSummary
|
||
? serverSummary.totalQuantity || serverSummary.itemsCount || 0
|
||
: cart.reduce((total, item) => total + item.quantity, 0);
|
||
|
||
// دیزاین حالت سبد خرید خالی
|
||
if (displayCart.length === 0) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50/50 flex flex-col items-center justify-center p-6">
|
||
<div className="bg-white p-12 rounded-[3rem] shadow-sm border border-gray-100 flex flex-col items-center max-w-md w-full text-center">
|
||
<div className="bg-blue-50/50 w-32 h-32 rounded-full flex items-center justify-center mb-8 relative">
|
||
<ShoppingBag size={56} className="text-blue-500 relative z-10" />
|
||
<div className="absolute inset-0 bg-blue-100 rounded-full animate-ping opacity-20"></div>
|
||
</div>
|
||
<h2 className="text-2xl font-black text-gray-800 mb-3">سبد خرید شما خالی است!</h2>
|
||
<p className="text-gray-500 mb-10 leading-relaxed text-sm">
|
||
هنوز هیچ محصولی به سبد خرید خود اضافه نکردهاید. برای مشاهده محصولات به صفحه اصلی برگردید.
|
||
</p>
|
||
<Link href="/products" className="w-full bg-[#ffb900] hover:bg-[#e5a600] text-black font-bold text-lg px-8 py-4 rounded-2xl transition-all shadow-[0_4px_20px_rgba(255,185,0,0.3)] hover:shadow-[0_6px_25px_rgba(255,185,0,0.4)] flex items-center justify-center gap-2">
|
||
بازگشت به فروشگاه
|
||
<ChevronLeft size={20} />
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<main className="bg-gray-50/30 min-h-screen pb-20">
|
||
{showNotLogin && (
|
||
<NotLogin
|
||
buttonText="بازگشت به سبد خرید"
|
||
onClose={() => setShowNotLogin(false)} />
|
||
)}
|
||
|
||
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
||
|
||
<div className="mb-10">
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h1 className="text-xl md:text-3xl font-black text-gray-800 flex items-center gap-3">
|
||
سبد خرید
|
||
<span className="bg-[#ffb900]/20 text-[#d99d00] text-xs md:text-sm font-bold py-1 px-3 rounded-xl flex items-center">
|
||
{totalItems} کالا
|
||
</span>
|
||
</h1>
|
||
</div>
|
||
|
||
<div className="mb-10">
|
||
<div className="relative w-full max-w-2xl mx-auto px-2 sm:px-0 mb-12">
|
||
<div className="absolute top-[20px] sm:top-[24px] left-[16.5%] right-[16.5%] h-[2px] bg-gray-200 z-0">
|
||
<div className="h-full bg-[#ffb900] w-[50%] transition-all duration-500 ease-in-out"></div>
|
||
</div>
|
||
|
||
<div className="flex justify-between relative z-10">
|
||
<Link href="/cart" className="flex flex-col items-center w-1/3 group cursor-pointer">
|
||
<div className=" w-10 h-10 sm:w-12 sm:h-12 bg-[#ffb900] text-[#1A2332] rounded-full flex items-center justify-center shadow-[0_0_15px_rgba(255,185,0,0.4)] mb-2 sm:mb-3 ring-[6px] ring-[#ffb900]/20 transition-all">
|
||
<ShoppingBag className="w-5 h-5 sm:w-6 sm:h-6" strokeWidth={2} />
|
||
</div>
|
||
<span className="text-[10px] sm:text-sm font-bold text-[#1A2332] text-center">سبد خرید</span>
|
||
</Link>
|
||
|
||
<div onClick={handleCheckoutNavigation} className="flex flex-col items-center w-1/3 cursor-pointer group">
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-white border-2 border-gray-200 text-gray-400 rounded-full flex items-center justify-center mb-2 sm:mb-3 ring-[6px] ring-[#f8fafc] sm:ring-[#f8fafc] group-hover:border-[#ffb900] transition-colors">
|
||
<Truck className="w-4 h-4 sm:w-5 sm:h-5" strokeWidth={2} />
|
||
</div>
|
||
<span className="text-[10px] sm:text-sm font-bold text-[#1A2332] text-center">اطلاعات ارسال</span>
|
||
</div>
|
||
|
||
<div className="flex flex-col items-center w-1/3">
|
||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-white border-2 border-gray-200 text-gray-400 rounded-full flex items-center justify-center mb-2 sm:mb-3 ring-[6px] ring-[#f8fafc] sm:ring-[#f8fafc]">
|
||
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5" />
|
||
</div>
|
||
<span className="text-[10px] sm:text-sm font-medium text-gray-400 text-center">پرداخت</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
|
||
<div className="flex-1">
|
||
<div className="bg-white rounded-[1rem] p-4 md:p-8 shadow-sm ">
|
||
<div className="flex justify-between items-center mb-6 pb-6 border-b border-gray-100">
|
||
<h2 className="text-base md:text-lg font-bold text-gray-800">محصولات انتخاب شده</h2>
|
||
<button onClick={handleClearAll} className="text-xs cursor-pointer md:text-sm text-red-500 hover:text-red-700 transition flex items-center gap-1.5 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-lg">
|
||
<Trash2 size={16} />
|
||
حذف همه
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-6">
|
||
{displayCart.map((item) => {
|
||
const itemTotal = parsePrice(item.price) * item.quantity;
|
||
return (
|
||
<div key={item.id} className="group flex flex-col sm:flex-row gap-4 sm:gap-6 items-start sm:items-center border-b border-gray-50 pb-6 last:border-0 last:pb-0">
|
||
<div className="bg-gray-50/80 p-3 rounded-2xl border border-gray-100 shrink-0 relative group-hover:bg-white transition-colors duration-300 w-full sm:w-auto flex justify-center">
|
||
<Image src={item.image} alt={item.title} width={100} height={100} className="object-contain w-24 h-24 drop-shadow-sm" />
|
||
</div>
|
||
<div className="flex-1 w-full">
|
||
<p className="text-xs font-medium text-blue-500 mb-1.5">{item.brand}</p>
|
||
<h3 className="text-sm md:text-base font-bold text-gray-800 line-clamp-2 leading-tight mb-3 group-hover:text-blue-600 transition-colors">{item.title}</h3>
|
||
</div>
|
||
<div className="flex sm:flex-col justify-between sm:justify-end items-center sm:items-end w-full sm:w-auto gap-4 shrink-0 mt-2 sm:mt-0">
|
||
<span className="font-black text-base md:text-lg text-gray-900">
|
||
{item.price ? `${itemTotal.toLocaleString('fa-IR')} ت` : 'استعلام'}
|
||
</span>
|
||
<CartControls
|
||
product={item.product as any}
|
||
cartItemId={isLoggedIn ? item.id : null}
|
||
quantity={item.quantity}
|
||
/>
|
||
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="w-full lg:w-[340px] shrink-0">
|
||
<div className="bg-white rounded-[1rem] p-4 md:p-8 shadow-sm sticky top-6">
|
||
<h2 className="text-lg md:text-xl font-bold text-gray-800 mb-6">خلاصه صورتحساب</h2>
|
||
<div className="space-y-4 mb-6">
|
||
<div className="flex justify-between items-center text-xs md:text-sm">
|
||
<span className="text-gray-500">تعداد کالاها</span>
|
||
<span className="font-bold text-gray-800">{totalItems} عدد</span>
|
||
</div>
|
||
<div className="flex justify-between items-center text-xs md:text-sm">
|
||
<span className="text-gray-500">هزینه ارسال</span>
|
||
<span className="text-blue-600 font-bold bg-blue-50 px-2 py-1 rounded-md text-[10px] md:text-xs">در مرحله بعد</span>
|
||
</div>
|
||
</div>
|
||
<div className="w-full border-t-2 border-dashed border-gray-200 my-6"></div>
|
||
<div className="flex justify-between items-center mb-8">
|
||
<span className="text-xs md:text-sm font-bold text-gray-600">مبلغ قابل پرداخت</span>
|
||
<span className="font-black text-xl md:text-2xl text-[#ffb900] tracking-tight">
|
||
{totalPrice > 0 ? `${totalPrice.toLocaleString('fa-IR')}` : 'استعلام'}
|
||
{totalPrice > 0 && <span className="text-xs md:text-sm font-medium text-gray-500 mr-1">تومان</span>}
|
||
</span>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleCheckoutNavigation}
|
||
className="w-full bg-[#ffb900] cursor-pointer hover:bg-[#e5a600] text-black py-3 md:py-4 rounded-xl font-bold text-base md:text-lg transition-all shadow-[0_4px_15px_rgba(255,185,0,0.2)] hover:shadow-[0_6px_20px_rgba(255,185,0,0.3)] flex justify-center items-center gap-2 mb-6"
|
||
>
|
||
تایید و ادامه
|
||
<ChevronLeft size={20} />
|
||
</button>
|
||
|
||
<div className="flex items-center justify-center gap-2 md:gap-4 text-[10px] md:text-xs font-medium text-gray-400 bg-gray-50 py-3 rounded-xl border border-gray-100">
|
||
<div className="flex items-center gap-1"><ShieldCheck size={14} className="text-green-500" /> پرداخت امن</div>
|
||
<div className="w-1 h-1 bg-gray-300 rounded-full"></div>
|
||
<div className="flex items-center gap-1"><Truck size={14} className="text-blue-500" /> ارسال سریع</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|