add cart API's (add , delete , get)

This commit is contained in:
haniyeroozmand
2026-04-07 10:14:56 +03:30
parent 5504e20948
commit 8be715b34b
14 changed files with 528 additions and 185 deletions

View File

@@ -4,7 +4,7 @@ import { useCart } from "@/components/context/cartcontext";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; // اضافه شدن useState import { useState, useEffect } from "react"; // اضافه شدن useEffect
import { import {
Trash2, Trash2,
ShoppingBag, ShoppingBag,
@@ -16,40 +16,140 @@ import {
CreditCard CreditCard
} from "lucide-react"; } from "lucide-react";
// مسیر ایمپورت کامپوننت Notlogin را بر اساس ساختار پوشه‌بندی خود تنظیم کنید
import NotLogin from "@/components/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() { 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 router = useRouter();
// استیت برای مدیریت نمایش مودال/کامپوننت لاگین نشده‌ها
const [showNotLogin, setShowNotLogin] = useState(false); 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 handleCheckoutNavigation = () => {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
if (token) { if (token) {
// اگر لاگین بود برود به چک اوت
router.push('/checkout'); router.push('/checkout');
} else { } else {
// اگر لاگین نبود، کامپوننت Notlogin نمایش داده شود
setShowNotLogin(true); setShowNotLogin(true);
} }
}; };
// تبدیل رشته قیمت به عدد // تبدیل رشته قیمت به عدد
const parsePrice = (priceStr?: number | null) => { const parsePrice = (priceStr?: number | string | null) => {
if (!priceStr) return 0; if (!priceStr) return 0;
return Number(priceStr.toString().replace(/,/g, '')); return Number(priceStr.toString().replace(/,/g, ''));
}; };
// محاسبه قیمت کل و تعداد کل // --- منطق یکپارچه‌سازی: اگر لاگین بود دیتای سرور، در غیر این‌صورت دیتای لوکال ---
const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0); const displayCart = isLoggedIn
const totalItems = cart.reduce((total, item) => total + item.quantity, 0); ? 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 ( return (
<div className="min-h-screen bg-gray-50/50 flex flex-col items-center justify-center p-6"> <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-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 className="text-gray-500 mb-10 leading-relaxed text-sm">
هنوز هیچ محصولی به سبد خرید خود اضافه نکردهاید. برای مشاهده محصولات به صفحه اصلی برگردید. هنوز هیچ محصولی به سبد خرید خود اضافه نکردهاید. برای مشاهده محصولات به صفحه اصلی برگردید.
</p> </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} /> <ChevronLeft size={20} />
</Link> </Link>
@@ -72,11 +172,9 @@ export default function CartPage() {
return ( return (
<main className="bg-gray-50/30 min-h-screen pb-20"> <main className="bg-gray-50/30 min-h-screen pb-20">
{/* رندر کردن کامپوننت Notlogin به صورت شرطی */}
{/* اگر نیاز است که کاربر بتواند آن را ببندد، پراپ onClose را به آن پاس بدهید */}
{showNotLogin && ( {showNotLogin && (
<NotLogin <NotLogin
buttonText="بازگشت به سبد خرید" buttonText="بازگشت به سبد خرید"
onClose={() => setShowNotLogin(false)} /> 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> <span className="text-[10px] sm:text-sm font-bold text-[#1A2332] text-center">سبد خرید</span>
</Link> </Link>
{/* کلیک روی آیکون اطلاعات ارسال */}
<div onClick={handleCheckoutNavigation} className="flex flex-col items-center w-1/3 cursor-pointer group"> <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"> <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} /> <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="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"> <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> <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} /> <Trash2 size={16} />
حذف همه حذف همه
</button> </button>
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{cart.map((item) => { {/* حلقه مپ روی displayCart اعمال شده است */}
{displayCart.map((item) => {
const itemTotal = parsePrice(item.price) * item.quantity; const itemTotal = parsePrice(item.price) * item.quantity;
return ( 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 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')} ت` : 'استعلام'} {item.price ? `${itemTotal.toLocaleString('fa-IR')} ت` : 'استعلام'}
</span> </span>
<div className="flex items-center gap-1 bg-gray-50 border border-gray-200 rounded-full p-1 shadow-sm"> <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} /> <Plus size={14} strokeWidth={2.5} />
</button> </button>
<span className="text-xs md:text-sm font-bold text-gray-800 w-6 text-center">{item.quantity}</span> <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> </span>
</div> </div>
{/* دکمه تایید و ادامه */}
<button <button
onClick={handleCheckoutNavigation} 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} /> <ChevronLeft size={20} />

View File

@@ -268,14 +268,13 @@ export default function Home() {
</div> </div>
</div> </div>
{/* 4. نمایش وضعیت لودینگ یا لیست محصولات */}
{loading ? ( {loading ? (
<div className="flex justify-center py-10"> <div className="flex justify-center py-10">
<p>در حال بارگذاری محصولات...</p> <p>در حال بارگذاری محصولات...</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> <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} /> <ProductCard key={product.id} product={product} />
))} ))}
</div> </div>

View File

@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
import Image from "next/image"; import Image from "next/image";
import ProductCartAction from '@/components/cartaction'; import ProductCartAction from '@/components/cartaction';
import { getProductBySlug } from "@/public/src/services/products/api"; import { getProductBySlug } from "@/public/src/services/products/api";
import Link from 'next/link';
interface PageProps { interface PageProps {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
@@ -10,7 +11,7 @@ interface PageProps {
export default async function SingleProductPage({ params }: PageProps) { export default async function SingleProductPage({ params }: PageProps) {
const resolvedParams = await params; const resolvedParams = await params;
const slug = resolvedParams.slug; const slug = resolvedParams.slug;
console.log("👉 Extracted slug from URL:", slug); console.log("👉 Extracted slug from URL:", slug);
@@ -47,18 +48,18 @@ export default async function SingleProductPage({ params }: PageProps) {
return ( return (
<div className="bg-[#f8f9fc] min-h-screen py-8" dir="rtl"> <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 /> <ScrollToTop />
{/* مسیر راهنما */} {/* مسیر راهنما */}
<div className="text-sm text-gray-500 mb-6 flex items-center gap-2"> <div className="text-sm text-gray-500 mb-6 flex items-center gap-2">
<span>خانه</span> &gt; <span>محصولات</span> &gt; <span className="text-gray-800 font-semibold">{product.title}</span> <Link href={'/'}>خانه</Link> &gt; <Link href={'/products'}>محصولات</Link> &gt; <span className="text-gray-800 font-semibold">{product.title}</span>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
{/* بخش 1: معرفی محصول */} {/* بخش 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="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"> <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"> <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} {product.brand}
</div> </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} {product.title}
</h1> </h1>
@@ -89,20 +90,20 @@ export default async function SingleProductPage({ params }: PageProps) {
</p> </p>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* رندر داینامیک ۳ ویژگی اول از API */} {/* رندر داینامیک ۳ ویژگی اول از API */}
{product.attributes?.slice(0, 3).map((attr: any) => ( {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"> <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="text-gray-500 text-sm mb-2">{attr.name}</span>
<span className="font-bold text-gray-800 text-sm" dir="ltr">{attr.valueText || '-'}</span> <span className="font-bold text-gray-800 text-xs" dir="ltr">{attr.valueText || '-'}</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"> {/* <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="text-gray-500 text-sm mb-2">دسته‌بندی</span>
<span className="font-bold text-gray-800 text-sm" dir="ltr">{product.primaryCategory?.name || '-'}</span> <span className="font-bold text-gray-800 text-xs" dir="ltr">{product.primaryCategory?.name || '-'}</span>
</div> </div> */}
</div> </div>
</div> </div>
</div> </div>
@@ -110,7 +111,7 @@ export default async function SingleProductPage({ params }: PageProps) {
{/* بخش 2: سایدبار و دکمه خرید */} {/* بخش 2: سایدبار و دکمه خرید */}
<div className="lg:col-span-4 col-span-2 lg:row-span-2 order-2 lg:order-none relative h-full"> <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"> <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 ? ( {hasStock ? (
@@ -180,7 +181,7 @@ export default async function SingleProductPage({ params }: PageProps) {
</div> </div>
{/* بخش 3: مشخصات ابعادی */} {/* بخش 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="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"> <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> <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"> <div className="w-full md:w-1/2">
<table className="w-full text-right text-sm"> <table className="w-full text-right text-sm">
<tbody> <tbody>
<tr className="border-b border-gray-50"> {product.attributes?.map((attr: any) => (
<td className="py-3 text-gray-500">قطر داخلی ($d$)</td> <tr key={attr.id}>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('قطر داخلی')}</td> <td className="py-3 text-[0.9em] text-gray-500">{attr.name}:</td>
</tr> <td className="py-3 font-bold text-[0.9em] text-gray-800 text-left" dir="ltr">{attr.valueText || '-'}</td>
<tr className="border-b border-gray-50"> </tr>
<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>
<tr> <tr>
<td className="py-3 text-gray-500">کد بینالمللی</td> <td className="py-3 text-[0.9em] text-gray-500">کد بینالمللی:</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.technicalCode}</td> <td className="py-3 font-bold text-[0.9em] text-gray-800 text-left" dir="ltr">{product.technicalCode}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -26,3 +26,4 @@ export default async function ProductsPage() {
</div> </div>
); );
} }

View File

@@ -57,7 +57,7 @@ export default function ProductGrid({ products }: { products: any[] }) {
<ClientPagination <ClientPagination
totalPages={totalPages} totalPages={totalPages}
currentPage={currentPage} currentPage={currentPage}
onPageChange={handlePageChange} onPageChangeAction={handlePageChange}
/> />
</> </>
); );

View File

@@ -3,17 +3,6 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Product } from '@/public/src/types/product'; 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 { export interface CartItem extends Product {
quantity: number; quantity: number;

View File

@@ -11,6 +11,7 @@ import { registerUser } from '@/public/src/services/auth/api';
import { loginUser } from '@/public/src/services/auth/api'; import { loginUser } from '@/public/src/services/auth/api';
import { logoutUser } from '@/public/src/services/auth/api'; import { logoutUser } from '@/public/src/services/auth/api';
import { useCategories } from './context/categoryprovider'; import { useCategories } from './context/categoryprovider';
import { getCartApi } from '@/public/src/services/cart/api';
const topBarLinks = [ const topBarLinks = [
{ label: "بخش صنعتی", href: "/" }, { label: "بخش صنعتی", href: "/" },
@@ -129,7 +130,48 @@ export function Header() {
const [showLoginPassword, setShowLoginPassword] = useState(false); const [showLoginPassword, setShowLoginPassword] = useState(false);
const [loginMobile, setLoginMobile] = useState(""); const [loginMobile, setLoginMobile] = useState("");
const [showRegisterSuccessDialog, setShowRegisterSuccessDialog] = useState(false); 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({ const [loginForm, setLoginForm] = useState({
@@ -212,9 +254,9 @@ export function Header() {
localStorage.setItem("username", username); localStorage.setItem("username", username);
localStorage.setItem("fullName", displayName); localStorage.setItem("fullName", displayName);
localStorage.setItem("role", role.toLowerCase()); localStorage.setItem("role", role.toLowerCase());
console.log(res); console.log(res);
setUser({ username, displayName }); setUser({ username, displayName });
setIsOpen(false); setIsOpen(false);
setShowRegisterSuccessDialog(true); setShowRegisterSuccessDialog(true);
@@ -267,7 +309,7 @@ export function Header() {
localStorage.setItem("refreshToken", res.data.refreshToken); localStorage.setItem("refreshToken", res.data.refreshToken);
localStorage.setItem("username", username); localStorage.setItem("username", username);
localStorage.setItem("fullName", displayName); localStorage.setItem("fullName", displayName);
console.log(res); console.log(res);
setUser({ username, displayName }); setUser({ username, displayName });
setIsOpen(false); setIsOpen(false);
router.push("/dashboard?success=login"); 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 ( return (
<header> <header>
<div> <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"> <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} /> <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"> <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> </span>
)} )}
</Link> </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-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"> <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"> <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" /> <ShoppingCart className="w-8 h-8 text-gray-200" />
<span>سبد خرید شما خالی است</span> <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"> <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> <Link href="/cart" className="text-xs text-blue-500 hover:text-blue-700 font-medium transition">مشاهده سبد خرید</Link>
</div> </div>
<div className="max-h-64 overflow-y-auto p-2 flex flex-col gap-1 custom-scrollbar"> <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; const itemTotal = item.price ? (Number(item.price.toString().replace(/,/g, '')) * item.quantity).toLocaleString('fa-IR') : null;
return ( return (
<div key={item.id} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition group/item relative"> <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> </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>
<div className="p-4 bg-white border-t border-gray-100"> <div className="p-4 bg-white border-t border-gray-100">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<span className="text-xs text-gray-500">مبلغ قابل پرداخت:</span> <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> </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> <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> </div>
@@ -633,6 +802,7 @@ export function Header() {
</div> </div>
</div> </div>
{!user ? ( {!user ? (
<button <button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}

View File

@@ -3,10 +3,10 @@
interface PaginationProps { interface PaginationProps {
totalPages: number; totalPages: number;
currentPage: 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; if (totalPages <= 1) return null;
const getPaginationItems = () => { 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"> <div className="flex justify-center items-center gap-2 mt-10 mb-6 flex-wrap" dir="rtl">
{/* دکمه قبلی */} {/* دکمه قبلی */}
<button <button
onClick={() => onPageChange(currentPage - 1)} onClick={() => onPageChangeAction(currentPage - 1)}
disabled={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" 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 ( return (
<button <button
key={index} 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 ${ 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 currentPage === page
? 'bg-[#ffb900] text-[#1A2332] font-bold shadow-lg shadow-[#ffb900]/30 scale-105' // استایل دکمه فعال (زرد) ? '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 <button
onClick={() => onPageChange(currentPage + 1)} onClick={() => onPageChangeAction(currentPage + 1)}
disabled={currentPage >= totalPages} 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" 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"
> >

View File

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

View File

@@ -5,13 +5,12 @@ import Image from "next/image";
import Link from 'next/link'; import Link from 'next/link';
import { useCart } from "./context/cartcontext"; import { useCart } from "./context/cartcontext";
import { Product } from "@/public/src/types/product"; import { Product } from "@/public/src/types/product";
import { addToCartApi } from "@/public/src/services/cart/api";
interface ProductCardProps { interface ProductCardProps {
product: Product; product: Product;
} }
export default function ProductCard({ product }: ProductCardProps) { export default function ProductCard({ product }: ProductCardProps) {
const { addToCart, decreaseQuantity, cart } = useCart(); const { addToCart, decreaseQuantity, cart } = useCart();
@@ -19,17 +18,45 @@ export default function ProductCard({ product }: ProductCardProps) {
const quantity = cartItem ? cartItem.quantity : 0; const quantity = cartItem ? cartItem.quantity : 0;
const slug = product.slug; const slug = product.slug;
const handleIncrease = async (e: React.MouseEvent) => {
const handleIncrease = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// ۱. آپدیت آنی سبد خرید محلی (این کار باعث آپدیت درجا در UI می‌شود)
addToCart(product); 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
decreaseQuantity(product.id); 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 const formattedPrice = product.price
@@ -37,7 +64,6 @@ export default function ProductCard({ product }: ProductCardProps) {
: null; : null;
return ( 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"> <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> <p className="text-[#808080] text-sm">{product.brand}</p>
<h3 className="text-sm font-semibold">{product.title}</h3> <h3 className="text-sm font-semibold">{product.title}</h3>
<div className="text-xs text-gray-500 mt-2"> <div className="text-xs text-gray-500 mt-2">
{/*
✅ این بخش را تغییر می‌دهیم تا نام attribute ها را بگیریم
اگر attribute اول وجود داشت، نامش را بگیر و نمایش بده
اگر attribute اول نبود، نمایش نده
*/}
{product.attributes?.[0] && ( {product.attributes?.[0] && (
<div className="flex border-b border-[#dfdfdf] mb-2 pb-2 justify-between"> <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 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> {/* 👈 مقدار attribute اول */} <p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[0].valueText || "-"}</p>
</div> </div>
)} )}
{/*
✅ مشابه بالا برای attribute دوم
*/}
{product.attributes?.[1] && ( {product.attributes?.[1] && (
<div className="flex justify-between"> <div className="flex justify-between">
<p className="text-[0.9em]">{product.attributes[1].name}:</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> {/* 👈 مقدار attribute دوم */} <p dir="rtl" className="font-semibold text-left text-[0.8em] text-black">{product.attributes[1].valueText || "-"}</p>
</div> </div>
)} )}
</div> </div>
{product.price ? ( {product.price ? (
<div className="flex justify-between mt-3 items-center min-h-[32px]"> <div className="flex justify-between mt-3 items-center min-h-[32px]">
<span className="text-[0.85em] font-bold text-gray-800"> <span className="text-[0.85em] font-bold text-gray-800">

View File

@@ -3,6 +3,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
reactCompiler: true, reactCompiler: true,
images: {
domains: ['s3.ir-thr-at1.arvanstorage.ir'], // add this line
},
}; };
export default nextConfig; export default nextConfig;

View 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
};
}

View 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();
};

View File

@@ -92,3 +92,26 @@ export async function getProductBySlug(slug: string) {
return null; 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;
}
}