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

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

View File

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

View File

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

View File

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

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