add cart API's (add , delete , get)
This commit is contained in:
@@ -4,7 +4,7 @@ import { useCart } from "@/components/context/cartcontext";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react"; // اضافه شدن useState
|
||||
import { useState, useEffect } from "react"; // اضافه شدن useEffect
|
||||
import {
|
||||
Trash2,
|
||||
ShoppingBag,
|
||||
@@ -16,40 +16,140 @@ import {
|
||||
CreditCard
|
||||
} from "lucide-react";
|
||||
|
||||
// مسیر ایمپورت کامپوننت Notlogin را بر اساس ساختار پوشهبندی خود تنظیم کنید
|
||||
import NotLogin from "@/components/Notlogin";
|
||||
import { getCartApi } from "@/public/src/services/cart/api"; // ایمپورت API
|
||||
import { clearServerCartApi } from "@/public/src/services/cart/api";
|
||||
|
||||
export default function CartPage() {
|
||||
const { cart, clearCart, addToCart, decreaseQuantity } = useCart();
|
||||
const {
|
||||
cart,
|
||||
clearCart,
|
||||
addToCart,
|
||||
decreaseQuantity,
|
||||
} = 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);
|
||||
|
||||
// --- استیتهای مربوط به دریافت اطلاعات سرور ---
|
||||
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);
|
||||
|
||||
// ۲. ارسال دستور مستقیم به هدر برای خالی شدن درجا (بدون درنگ)
|
||||
window.dispatchEvent(new Event('cartCleared'));
|
||||
|
||||
// ۳. فراخوانی API برای پاک کردن دیتابیس در پسزمینه
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null;
|
||||
if (token) {
|
||||
try {
|
||||
await clearServerCartApi(); // متد DELETE
|
||||
// اینجا دیگر نیازی به cartUpdated نیست چون هدر را به صورت دستی خالی کردیم
|
||||
} catch (error) {
|
||||
console.error("خطا در پاک کردن سبد خرید سرور:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- useEffect برای بررسی لاگین و دریافت دیتای سرور ---
|
||||
useEffect(() => {
|
||||
const fetchServerCart = 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);
|
||||
}
|
||||
} else {
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchServerCart();
|
||||
}, []);
|
||||
|
||||
|
||||
// بررسی لاگین کاربر
|
||||
const handleCheckoutNavigation = () => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
// اگر لاگین بود برود به چک اوت
|
||||
router.push('/checkout');
|
||||
} else {
|
||||
// اگر لاگین نبود، کامپوننت Notlogin نمایش داده شود
|
||||
setShowNotLogin(true);
|
||||
}
|
||||
};
|
||||
|
||||
// تبدیل رشته قیمت به عدد
|
||||
const parsePrice = (priceStr?: number | null) => {
|
||||
const parsePrice = (priceStr?: number | string | null) => {
|
||||
if (!priceStr) return 0;
|
||||
return Number(priceStr.toString().replace(/,/g, ''));
|
||||
};
|
||||
|
||||
// محاسبه قیمت کل و تعداد کل
|
||||
const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
|
||||
const totalItems = cart.reduce((total, item) => total + item.quantity, 0);
|
||||
// --- منطق یکپارچهسازی: اگر لاگین بود دیتای سرور، در غیر اینصورت دیتای لوکال ---
|
||||
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;
|
||||
|
||||
// دیزاین حالت سبد خرید خالی
|
||||
if (cart.length === 0) {
|
||||
// محاسبه قیمت کل و تعداد کل
|
||||
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 (isLoading) {
|
||||
return <div className="min-h-screen bg-gray-50/50 flex items-center justify-center">در حال بارگذاری...</div>;
|
||||
}
|
||||
|
||||
// دیزاین حالت سبد خرید خالی (تغییر cart.length به displayCart.length)
|
||||
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">
|
||||
@@ -61,7 +161,7 @@ export default function CartPage() {
|
||||
<p className="text-gray-500 mb-10 leading-relaxed text-sm">
|
||||
هنوز هیچ محصولی به سبد خرید خود اضافه نکردهاید. برای مشاهده محصولات به صفحه اصلی برگردید.
|
||||
</p>
|
||||
<Link href="/" 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">
|
||||
<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>
|
||||
@@ -72,11 +172,9 @@ export default function CartPage() {
|
||||
|
||||
return (
|
||||
<main className="bg-gray-50/30 min-h-screen pb-20">
|
||||
{/* رندر کردن کامپوننت Notlogin به صورت شرطی */}
|
||||
{/* اگر نیاز است که کاربر بتواند آن را ببندد، پراپ onClose را به آن پاس بدهید */}
|
||||
{showNotLogin && (
|
||||
<NotLogin
|
||||
buttonText="بازگشت به سبد خرید"
|
||||
<NotLogin
|
||||
buttonText="بازگشت به سبد خرید"
|
||||
onClose={() => setShowNotLogin(false)} />
|
||||
)}
|
||||
|
||||
@@ -106,7 +204,6 @@ export default function CartPage() {
|
||||
<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} />
|
||||
@@ -130,14 +227,15 @@ export default function CartPage() {
|
||||
<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={clearCart} className="text-xs 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">
|
||||
<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">
|
||||
{cart.map((item) => {
|
||||
{/* حلقه مپ روی displayCart اعمال شده است */}
|
||||
{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">
|
||||
@@ -153,7 +251,7 @@ export default function CartPage() {
|
||||
{item.price ? `${itemTotal.toLocaleString('fa-IR')} ت` : 'استعلام'}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 bg-gray-50 border border-gray-200 rounded-full p-1 shadow-sm">
|
||||
<button onClick={() => addToCart(item)} className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center rounded-full bg-white text-gray-600 hover:text-green-600 hover:shadow-sm transition-all">
|
||||
<button onClick={() => addToCart(item as any)} className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center rounded-full bg-white text-gray-600 hover:text-green-600 hover:shadow-sm transition-all">
|
||||
<Plus size={14} strokeWidth={2.5} />
|
||||
</button>
|
||||
<span className="text-xs md:text-sm font-bold text-gray-800 w-6 text-center">{item.quantity}</span>
|
||||
@@ -191,10 +289,9 @@ export default function CartPage() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* دکمه تایید و ادامه */}
|
||||
<button
|
||||
onClick={handleCheckoutNavigation}
|
||||
className="w-full bg-[#ffb900] 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"
|
||||
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} />
|
||||
|
||||
@@ -268,14 +268,13 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. نمایش وضعیت لودینگ یا لیست محصولات */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<p>در حال بارگذاری محصولات...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{products.slice(-4).map((product: any) => (
|
||||
{products.slice(0,4).map((product: any) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
|
||||
import Image from "next/image";
|
||||
import ProductCartAction from '@/components/cartaction';
|
||||
import { getProductBySlug } from "@/public/src/services/products/api";
|
||||
import Link from 'next/link';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -10,7 +11,7 @@ interface PageProps {
|
||||
|
||||
export default async function SingleProductPage({ params }: PageProps) {
|
||||
|
||||
const resolvedParams = await params;
|
||||
const resolvedParams = await params;
|
||||
const slug = resolvedParams.slug;
|
||||
|
||||
console.log("👉 Extracted slug from URL:", slug);
|
||||
@@ -47,18 +48,18 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
|
||||
return (
|
||||
<div className="bg-[#f8f9fc] min-h-screen py-8" dir="rtl">
|
||||
<div className="mx-auto px-4 lg:px-8 container max-w-6xl">
|
||||
<div className="mx-auto px-4 container max-w-6xl">
|
||||
<ScrollToTop />
|
||||
|
||||
{/* مسیر راهنما */}
|
||||
<div className="text-sm text-gray-500 mb-6 flex items-center gap-2">
|
||||
<span>خانه</span> > <span>محصولات</span> > <span className="text-gray-800 font-semibold">{product.title}</span>
|
||||
<Link href={'/'}>خانه</Link> > <Link href={'/products'}>محصولات</Link> > <span className="text-gray-800 font-semibold">{product.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
|
||||
|
||||
|
||||
{/* بخش 1: معرفی محصول */}
|
||||
<div className="lg:col-span-8 order-1 lg:order-none">
|
||||
<div className="lg:col-span-8 col-span-2 order-1 lg:order-none">
|
||||
<div className="bg-white rounded-3xl shadow-sm p-6 md:p-8 flex flex-col md:flex-row gap-8 items-center border border-gray-100">
|
||||
{/* باکس تصویر */}
|
||||
<div className="w-full md:w-[52%] h-72 rounded-2xl bg-[linear-gradient(135deg,#f8f9fc_0%,#e2e8f0_100%)] flex items-center justify-center p-6 relative">
|
||||
@@ -80,7 +81,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
<div className="inline-block px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-sm font-bold mb-4 tracking-wider">
|
||||
{product.brand}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-6 leading-relaxed">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6 leading-relaxed">
|
||||
{product.title}
|
||||
</h1>
|
||||
|
||||
@@ -89,20 +90,20 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* رندر داینامیک ۳ ویژگی اول از API */}
|
||||
{product.attributes?.slice(0, 3).map((attr: any) => (
|
||||
<div key={attr.id} className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
||||
<span className="text-gray-500 text-sm mb-2">{attr.name}</span>
|
||||
<span className="font-bold text-gray-800 text-sm" dir="ltr">{attr.valueText || '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
{/* رندر داینامیک ۳ ویژگی اول از API */}
|
||||
{product.attributes?.slice(0, 2).map((attr: any) => (
|
||||
<div key={attr.id} className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
||||
<span className="text-gray-500 text-sm mb-2">{attr.name}</span>
|
||||
<span className="font-bold text-gray-800 text-xs" dir="ltr">{attr.valueText || '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* آیتم چهارم: دستهبندی (برای حفظ ظاهر گرید ۴ تایی) */}
|
||||
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
||||
<span className="text-gray-500 text-sm mb-2">دستهبندی</span>
|
||||
<span className="font-bold text-gray-800 text-sm" dir="ltr">{product.primaryCategory?.name || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* آیتم چهارم: دستهبندی (برای حفظ ظاهر گرید ۴ تایی) */}
|
||||
{/* <div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
||||
<span className="text-gray-500 text-sm mb-2">دستهبندی</span>
|
||||
<span className="font-bold text-gray-800 text-xs" dir="ltr">{product.primaryCategory?.name || '-'}</span>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +111,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
|
||||
{/* بخش 2: سایدبار و دکمه خرید */}
|
||||
<div className="lg:col-span-4 col-span-2 lg:row-span-2 order-2 lg:order-none relative h-full">
|
||||
<div className="flex flex-col gap-4 lg:sticky lg:top-[20px] pb-8">
|
||||
<div className="flex flex-col gap-4 lg:sticky lg:top-[20px]">
|
||||
|
||||
<div className="bg-white rounded-[2rem] shadow-[0_4px_20px_-10px_rgba(0,0,0,0.05)] p-5 border border-gray-100 text-center">
|
||||
{hasStock ? (
|
||||
@@ -180,7 +181,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
</div>
|
||||
|
||||
{/* بخش 3: مشخصات ابعادی */}
|
||||
<div className="lg:col-span-8 order-3 lg:order-none">
|
||||
<div className="lg:col-span-8 col-span-2 order-3 lg:order-none">
|
||||
<div className="bg-white rounded-3xl shadow-sm p-6 md:p-8 border border-gray-100">
|
||||
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
||||
<h2 className="text-xl font-bold text-gray-800">مشخصات ابعادی دقیق</h2>
|
||||
@@ -206,29 +207,16 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
<div className="w-full md:w-1/2">
|
||||
<table className="w-full text-right text-sm">
|
||||
<tbody>
|
||||
<tr className="border-b border-gray-50">
|
||||
<td className="py-3 text-gray-500">قطر داخلی ($d$)</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('قطر داخلی')}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-50">
|
||||
<td className="py-3 text-gray-500">قطر خارجی ($D$)</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('قطر خارجی')}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-50">
|
||||
<td className="py-3 text-gray-500">پهنا ($B$)</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('پهنا')}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-50">
|
||||
<td className="py-3 text-gray-500">وزن خالص</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('وزن')}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-50">
|
||||
<td className="py-3 text-gray-500">جنس قفسه</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('جنس قفسه')}</td>
|
||||
</tr>
|
||||
{product.attributes?.map((attr: any) => (
|
||||
<tr key={attr.id}>
|
||||
<td className="py-3 text-[0.9em] text-gray-500">{attr.name}:</td>
|
||||
<td className="py-3 font-bold text-[0.9em] text-gray-800 text-left" dir="ltr">{attr.valueText || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
<tr>
|
||||
<td className="py-3 text-gray-500">کد بینالمللی</td>
|
||||
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.technicalCode}</td>
|
||||
<td className="py-3 text-[0.9em] text-gray-500">کد بینالمللی:</td>
|
||||
<td className="py-3 font-bold text-[0.9em] text-gray-800 text-left" dir="ltr">{product.technicalCode}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -26,3 +26,4 @@ export default async function ProductsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ProductGrid({ products }: { products: any[] }) {
|
||||
<ClientPagination
|
||||
totalPages={totalPages}
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
onPageChangeAction={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,17 +3,6 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Product } from '@/public/src/types/product';
|
||||
|
||||
// export interface Product {
|
||||
// id: string;
|
||||
// title: string;
|
||||
// image: string;
|
||||
// l: string;
|
||||
// d: string;
|
||||
// brand: string;
|
||||
// price?: string | null;
|
||||
// badge?: string;
|
||||
// stock: boolean;
|
||||
// }
|
||||
|
||||
export interface CartItem extends Product {
|
||||
quantity: number;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { registerUser } from '@/public/src/services/auth/api';
|
||||
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';
|
||||
|
||||
const topBarLinks = [
|
||||
{ label: "بخش صنعتی", href: "/" },
|
||||
@@ -129,7 +130,48 @@ export function Header() {
|
||||
const [showLoginPassword, setShowLoginPassword] = useState(false);
|
||||
const [loginMobile, setLoginMobile] = useState("");
|
||||
const [showRegisterSuccessDialog, setShowRegisterSuccessDialog] = useState(false);
|
||||
const [isOptimistic, setIsOptimistic] = useState(false);
|
||||
// ۱. افکت اول: وقتی کانتکست لوکال (cart) با کلیک کاربر آپدیت میشود،
|
||||
// منو را به حالت Optimistic میبریم تا تغییرات را درجا نشان دهد.
|
||||
useEffect(() => {
|
||||
setIsOptimistic(true);
|
||||
// بعد از ۱.۵ ثانیه (زمانی که قاعدتاً API سرور کارش تمام شده) به حالت عادی برمیگردد
|
||||
const timer = setTimeout(() => setIsOptimistic(false), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [cart]);
|
||||
|
||||
// ۲. افکت دوم: دریافت اطلاعات سرور در پسزمینه بدون رفرش صفحه
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// گوش دادن به سیگنالی که از ProductCard ارسال میشود
|
||||
window.addEventListener('cartUpdated', fetchServerData);
|
||||
|
||||
// فراخوانی در لود اولیه
|
||||
fetchServerData();
|
||||
|
||||
return () => window.removeEventListener('cartUpdated', fetchServerData);
|
||||
}, []);
|
||||
|
||||
// محاسبه قیمت کل لوکال (برای زمانی که کاربر لاگین نیست یا در حالت Optimistic هستیم)
|
||||
const localTotalPrice = cart.reduce((total, item) => {
|
||||
const price = item.price ? Number(item.price.toString().replace(/,/g, '')) : 0;
|
||||
return total + (price * item.quantity);
|
||||
}, 0);
|
||||
|
||||
|
||||
const [loginForm, setLoginForm] = useState({
|
||||
@@ -212,9 +254,9 @@ export function Header() {
|
||||
localStorage.setItem("username", username);
|
||||
localStorage.setItem("fullName", displayName);
|
||||
localStorage.setItem("role", role.toLowerCase());
|
||||
console.log(res);
|
||||
console.log(res);
|
||||
|
||||
|
||||
|
||||
setUser({ username, displayName });
|
||||
setIsOpen(false);
|
||||
setShowRegisterSuccessDialog(true);
|
||||
@@ -267,7 +309,7 @@ export function Header() {
|
||||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
||||
localStorage.setItem("username", username);
|
||||
localStorage.setItem("fullName", displayName);
|
||||
console.log(res);
|
||||
console.log(res);
|
||||
setUser({ username, displayName });
|
||||
setIsOpen(false);
|
||||
router.push("/dashboard?success=login");
|
||||
@@ -373,6 +415,133 @@ export function Header() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
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); // کلینآپ لیسنر جدید
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
|
||||
|
||||
|
||||
// دریافت اطلاعات سبد خرید از سرور
|
||||
useEffect(() => {
|
||||
const fetchServerCart = 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);
|
||||
}
|
||||
} else {
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
};
|
||||
// --- این تابع جدید را اضافه کنید ---
|
||||
const handleCartClear = () => {
|
||||
setServerCartItems([]); // درجا لیست منو را خالی میکند
|
||||
setServerSummary(null);
|
||||
};
|
||||
// گوش دادن به رویدادها
|
||||
window.addEventListener('cartUpdated', fetchServerCart);
|
||||
window.addEventListener('cartCleared', handleCartClear); // لیسنر جدید
|
||||
|
||||
fetchServerCart();
|
||||
return () => {
|
||||
window.removeEventListener('cartUpdated', fetchServerCart);
|
||||
window.removeEventListener('cartCleared', handleCartClear); // کلینآپ لیسنر جدید
|
||||
};
|
||||
// در صورت نیاز به آپدیت شدن دراپداون با هر تغییر، میتوانید این تابع را به یک 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,
|
||||
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;
|
||||
|
||||
const displayTotalQuantity = (isLoggedIn && !isOptimistic) && serverSummary
|
||||
? serverSummary.totalQuantity || serverSummary.itemsCount || 0
|
||||
: cart.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
const displayTotalPrice = (isLoggedIn && !isOptimistic) && serverSummary
|
||||
? serverSummary.totalPrice || serverSummary.total || 0
|
||||
: localTotalPrice;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div>
|
||||
@@ -573,9 +742,9 @@ export function Header() {
|
||||
|
||||
<Link href="/cart" className="flex items-center justify-center p-2.5 bg-white border border-gray-300/60 rounded-xl text-gray-700 hover:bg-gray-50 transition relative z-10">
|
||||
<ShoppingCart className="w-4 h-4 text-gray-500" strokeWidth={1.8} />
|
||||
{cart.length > 0 && (
|
||||
{displayCart.length > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 bg-[#f92a35] text-white text-[10px] font-bold w-4 h-4 flex items-center justify-center rounded-full shadow-sm">
|
||||
{cart.reduce((total, item) => total + item.quantity, 0)}
|
||||
{displayTotalQuantity}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
@@ -583,7 +752,7 @@ export function Header() {
|
||||
<div className="absolute top-full left-0 w-full h-3 bg-transparent hidden group-hover:block"></div>
|
||||
|
||||
<div className="absolute top-[calc(100%+12px)] left-0 w-80 bg-white border border-gray-200 rounded-2xl shadow-xl hidden group-hover:flex flex-col overflow-hidden z-50">
|
||||
{cart.length === 0 ? (
|
||||
{displayCart.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-gray-500 flex flex-col items-center gap-2">
|
||||
<ShoppingCart className="w-8 h-8 text-gray-200" />
|
||||
<span>سبد خرید شما خالی است</span>
|
||||
@@ -591,11 +760,11 @@ export function Header() {
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center p-4 border-b border-gray-100 bg-gray-50/50">
|
||||
<span className="text-xs font-semibold text-gray-500">{cart.length} کالا</span>
|
||||
<span className="text-xs font-semibold text-gray-500">{displayCart.length} کالا</span>
|
||||
<Link href="/cart" className="text-xs text-blue-500 hover:text-blue-700 font-medium transition">مشاهده سبد خرید</Link>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto p-2 flex flex-col gap-1 custom-scrollbar">
|
||||
{cart.slice(0, 3).map((item) => {
|
||||
{displayCart.slice(0, 3).map((item) => {
|
||||
const itemTotal = item.price ? (Number(item.price.toString().replace(/,/g, '')) * item.quantity).toLocaleString('fa-IR') : null;
|
||||
return (
|
||||
<div key={item.id} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition group/item relative">
|
||||
@@ -619,12 +788,12 @@ export function Header() {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{cart.length > 3 && (<p className="text-center text-[10px] text-gray-400 py-2 border-t border-gray-50 mt-1"> و {cart.length - 3} کالای دیگر...</p>)}
|
||||
{displayCart.length > 3 && (<p className="text-center text-[10px] text-gray-400 py-2 border-t border-gray-50 mt-1"> و {displayCart.length - 3} کالای دیگر...</p>)}
|
||||
</div>
|
||||
<div className="p-4 bg-white border-t border-gray-100">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-xs text-gray-500">مبلغ قابل پرداخت:</span>
|
||||
<span className="text-sm font-bold text-gray-800">{totalPrice > 0 ? `${totalPrice.toLocaleString('fa-IR')} تومان` : 'استعلام'}</span>
|
||||
<span className="text-sm font-bold text-gray-800">{displayTotalPrice > 0 ? `${displayTotalPrice.toLocaleString('fa-IR')} تومان` : 'استعلام'}</span>
|
||||
</div>
|
||||
<Link href="/cart" className="flex items-center justify-center w-full py-2 bg-[#ffb900] hover:bg-[#e6a600] text-black text-xs font-semibold rounded-xl transition-colors">ثبت سفارش</Link>
|
||||
</div>
|
||||
@@ -633,6 +802,7 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{!user ? (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
interface PaginationProps {
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageChangeAction: (page: number) => void;
|
||||
}
|
||||
|
||||
export default function ClientPagination({ totalPages, currentPage, onPageChange }: PaginationProps) {
|
||||
export default function ClientPagination({ totalPages, currentPage, onPageChangeAction }: PaginationProps) {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
const getPaginationItems = () => {
|
||||
@@ -55,7 +55,7 @@ export default function ClientPagination({ totalPages, currentPage, onPageChange
|
||||
<div className="flex justify-center items-center gap-2 mt-10 mb-6 flex-wrap" dir="rtl">
|
||||
{/* دکمه قبلی */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
onClick={() => onPageChangeAction(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
className="px-3 cursor-pointer py-2 md:px-4 h-10 flex items-center justify-center rounded-xl disabled:opacity-40 disabled:cursor-not-allowed bg-[#1A2332] text-gray-300 hover:bg-[#1A2332]/80 hover:text-white transition-all duration-300 text-sm md:text-base font-medium"
|
||||
>
|
||||
@@ -76,7 +76,7 @@ export default function ClientPagination({ totalPages, currentPage, onPageChange
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => onPageChange(page as number)}
|
||||
onClick={() => onPageChangeAction(page as number)}
|
||||
className={`min-w-[40px] cursor-pointer h-10 flex items-center justify-center rounded-xl transition-all duration-300 text-sm md:text-base font-medium ${
|
||||
currentPage === page
|
||||
? 'bg-[#ffb900] text-[#1A2332] font-bold shadow-lg shadow-[#ffb900]/30 scale-105' // استایل دکمه فعال (زرد)
|
||||
@@ -91,7 +91,7 @@ export default function ClientPagination({ totalPages, currentPage, onPageChange
|
||||
|
||||
{/* دکمه بعدی */}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
onClick={() => onPageChangeAction(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className="px-3 cursor-pointer py-2 md:px-4 h-10 flex items-center justify-center rounded-xl disabled:opacity-40 disabled:cursor-not-allowed bg-[#1A2332] text-gray-300 hover:bg-[#1A2332]/80 hover:text-white transition-all duration-300 text-sm md:text-base font-medium"
|
||||
>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// components/FilterSidebar.tsx
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
|
||||
export default function FilterSidebar({ categories, brands }: { categories: any[], brands: any[] }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const currentCategory = searchParams.get('category');
|
||||
const currentBrand = searchParams.get('brand');
|
||||
|
||||
const handleFilter = (type: 'category' | 'brand', value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// اگر روی فیلتر فعال کلیک شد، آن را حذف کن، در غیر این صورت اضافه کن
|
||||
if (params.get(type) === value) {
|
||||
params.delete(type);
|
||||
} else {
|
||||
params.set(type, value);
|
||||
}
|
||||
|
||||
// هنگام تغییر فیلتر، کاربر را به صفحه اول برگردانید
|
||||
params.set('page', '1');
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow space-y-8">
|
||||
{/* بخش دستهبندیها */}
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-4 border-b pb-2">دستهبندیها</h3>
|
||||
<ul className="space-y-2">
|
||||
{categories.map((cat) => (
|
||||
<li key={cat.id}>
|
||||
<button
|
||||
onClick={() => handleFilter('category', cat.slug || cat.id)} // از slug یا id استفاده کنید
|
||||
className={`text-sm w-full text-right hover:text-blue-600 transition-colors ${
|
||||
currentCategory === (cat.slug || cat.id) ? 'text-blue-600 font-bold' : 'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{cat.title || cat.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* بخش برندها */}
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-4 border-b pb-2">برندها</h3>
|
||||
<ul className="space-y-2">
|
||||
{brands.map((brand) => (
|
||||
<li key={brand.id}>
|
||||
<button
|
||||
onClick={() => handleFilter('brand', brand.slug || brand.id)}
|
||||
className={`text-sm w-full text-right hover:text-blue-600 transition-colors ${
|
||||
currentBrand === (brand.slug || brand.id) ? 'text-blue-600 font-bold' : 'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{brand.title || brand.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ 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";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
|
||||
export default function ProductCard({ product }: ProductCardProps) {
|
||||
const { addToCart, decreaseQuantity, cart } = useCart();
|
||||
|
||||
@@ -19,17 +18,45 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
const quantity = cartItem ? cartItem.quantity : 0;
|
||||
const slug = product.slug;
|
||||
|
||||
|
||||
const handleIncrease = (e: React.MouseEvent) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrease = (e: React.MouseEvent) => {
|
||||
// --- هندلر کاهش / حذف از سبد خرید ---
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formattedPrice = product.price
|
||||
@@ -37,7 +64,6 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
: 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">
|
||||
|
||||
@@ -59,30 +85,21 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
<p className="text-[#808080] text-sm">{product.brand}</p>
|
||||
<h3 className="text-sm font-semibold">{product.title}</h3>
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
{/*
|
||||
✅ این بخش را تغییر میدهیم تا نام attribute ها را بگیریم
|
||||
اگر attribute اول وجود داشت، نامش را بگیر و نمایش بده
|
||||
اگر attribute اول نبود، نمایش نده
|
||||
*/}
|
||||
{product.attributes?.[0] && (
|
||||
<div className="flex border-b border-[#dfdfdf] mb-2 pb-2 justify-between">
|
||||
<p className="text-[0.9em]">{product.attributes[0].name}:</p> {/* 👈 نام attribute اول */}
|
||||
<p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[0].valueText || "-"}</p> {/* 👈 مقدار attribute اول */}
|
||||
<p className="text-[0.9em]">{product.attributes[0].name}:</p>
|
||||
<p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[0].valueText || "-"}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
✅ مشابه بالا برای attribute دوم
|
||||
*/}
|
||||
{product.attributes?.[1] && (
|
||||
<div className="flex justify-between">
|
||||
<p className="text-[0.9em]">{product.attributes[1].name}:</p> {/* 👈 نام attribute دوم */}
|
||||
<p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[1].valueText || "-"}</p> {/* 👈 مقدار attribute دوم */}
|
||||
<p className="text-[0.9em]">{product.attributes[1].name}:</p>
|
||||
<p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[1].valueText || "-"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{product.price ? (
|
||||
<div className="flex justify-between mt-3 items-center min-h-[32px]">
|
||||
<span className="text-[0.85em] font-bold text-gray-800">
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { NextConfig } from "next";
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactCompiler: true,
|
||||
images: {
|
||||
domains: ['s3.ir-thr-at1.arvanstorage.ir'], // add this line
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
24
public/src/hook/UseProductFilters.tsx
Normal file
24
public/src/hook/UseProductFilters.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
// hooks/useProductFilters.ts
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
|
||||
export function useProductFilters() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const setFilter = (key: string, value: string | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (value) params.set(key, value);
|
||||
else params.delete(key);
|
||||
|
||||
// بازگشت به صفحه اول هنگام تغییر فیلتر
|
||||
params.set('page', '1');
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
return {
|
||||
category: searchParams.get('category'),
|
||||
brand: searchParams.get('brand'),
|
||||
setFilter
|
||||
};
|
||||
}
|
||||
103
public/src/services/cart/api.tsx
Normal file
103
public/src/services/cart/api.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { API_BASE_URL } from "../config";
|
||||
|
||||
// تابع دریافت توکن از لوکال استوریج
|
||||
const getAuthHeaders = () => {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// ۱. API دریافت سبد خرید (GET)
|
||||
export async function getCartApi() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/users/me/cart`, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
|
||||
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; // برگرداندن بخش data که شامل items و summary است
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ۲. API افزودن محصول به سبد خرید (POST)
|
||||
export async function addToCartApi(productId: string, quantity: number = 1) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/users/me/cart/items`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ productId, 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);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/users/me/cart`, {
|
||||
method: 'DELETE',
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('خطا در پاک کردن سبد خرید سرور');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
@@ -92,3 +92,26 @@ export async function getProductBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategoryFilters(slug: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/products/categories/${slug}/filters?page=1&limit=20`;
|
||||
const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
||||
if (!res.ok) throw new Error("Failed to fetch category filters");
|
||||
return (await res.json()).data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// دریافت فیلترهای مرتبط با یک برند
|
||||
export async function getBrandFilters(slug: string) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/products/brands/${slug}/filters?page=1&limit=20`;
|
||||
const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
||||
if (!res.ok) throw new Error("Failed to fetch brand filters");
|
||||
return (await res.json()).data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user