pagination/filter product api

This commit is contained in:
haniyeroozmand
2026-04-02 21:13:40 +03:30
parent a2de32dfad
commit 5504e20948
9 changed files with 296 additions and 27 deletions

View File

@@ -9,9 +9,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;
@@ -28,10 +26,7 @@ export default async function SingleProductPage({ params }: PageProps) {
return notFound(); return notFound();
} }
// --------------------------------------------------------
// ۲. آماده‌سازی و مپ کردن داده‌های API برای UI
// --------------------------------------------------------
// فرمت کردن قیمت // فرمت کردن قیمت
const formattedPrice = product.display_price const formattedPrice = product.display_price
? product.display_price.toLocaleString('fa-IR') ? product.display_price.toLocaleString('fa-IR')
@@ -49,12 +44,10 @@ export default async function SingleProductPage({ params }: PageProps) {
return attr ? attr.valueText : "-"; return attr ? attr.valueText : "-";
}; };
// --------------------------------------------------------
// ۳. خروجی UI (بدون تغییر در ساختار و استایل‌ها)
// --------------------------------------------------------
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-7xl"> <div className="mx-auto px-4 lg:px-8 container max-w-6xl">
<ScrollToTop /> <ScrollToTop />
{/* مسیر راهنما */} {/* مسیر راهنما */}

View File

@@ -1,31 +1,28 @@
// app/products/page.tsx // app/products/page.tsx
import ProductCard from '@/components/productcard';
import { getProducts } from '@/public/src/services/products/api'; import { getProducts } from '@/public/src/services/products/api';
import ProductGrid from '@/components/clientProduct';
export default async function ProductsPage() { export default async function ProductsPage() {
const data = await getProducts(1, 20); // گرفتن تمام محصولات در سرور (چون بک‌اند شما لیمیت را نادیده می‌گیرد، همه را دریافت می‌کنیم)
const data = await getProducts(1, 100);
const products = data.items.map((p: any) => ({ const products = data.items?.map((p: any) => ({
id: p.id, id: p.id,
title: p.title, title: p.title,
brand: p.brand, brand: p.brand,
price: p.calculated_price, price: p.calculated_price,
stock: p.stock, stock: p.stock,
slug: p.slug, slug: p.slug,
image: p.mainImageUrl || "/placeholder.png", image: p.mainImageUrl || "/src/img/1.jpg",
attributes: p.attributes attributes: p.attributes
})); })) || [];
return ( return (
<div className="container mx-auto px-4 max-w-6xl"> <div className="container mx-auto px-4 max-w-6xl py-8">
<h1 className="text-2xl font-bold mb-6">همه محصولات</h1> <h1 className="text-2xl font-bold mb-6">همه محصولات</h1>
{/* پاس دادن کل محصولات به کامپوننت کلاینت */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> <ProductGrid products={products} />
{products.map((product: any) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,64 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams, usePathname } from 'next/navigation';
import ProductCard from '@/components/productcard';
import ClientPagination from './pagination';
export default function ProductGrid({ products }: { products: any[] }) {
const searchParams = useSearchParams();
const pathname = usePathname();
// ۱. گرفتن شماره صفحه از URL در زمان لود اولیه
const initialPage = Number(searchParams.get('page')) || 1;
const [currentPage, setCurrentPage] = useState(initialPage);
const limit = 12;
// محاسبه کل صفحات
const totalPages = Math.ceil(products.length / limit);
// ۲. تابعی برای تغییر همزمان State و آدرس URL
const handlePageChange = (page: number) => {
setCurrentPage(page);
// تغییر آدرس مرورگر بدون رفرش و بدون درگیر کردن سرور
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
window.history.pushState(null, '', `${pathname}?${params.toString()}`);
};
// اگر کاربر با دکمه‌های Back/Forward مرورگر جابجا شد، state آپدیت شود
useEffect(() => {
const pageFromUrl = Number(searchParams.get('page')) || 1;
setCurrentPage(pageFromUrl);
}, [searchParams]);
// برش دادن محصولات فقط در سمت کلاینت
const startIndex = (currentPage - 1) * limit;
const endIndex = startIndex + limit;
const paginatedProducts = products.slice(startIndex, endIndex);
if (products.length === 0) {
return (
<div className="text-center py-12 bg-gray-50 rounded-lg text-gray-500">
محصولی یافت نشد.
</div>
);
}
return (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{paginatedProducts.map((product: any) => (
<ProductCard key={product.id} product={product} />
))}
</div>
<ClientPagination
totalPages={totalPages}
currentPage={currentPage}
onPageChange={handlePageChange}
/>
</>
);
}

102
components/pagination.tsx Normal file
View File

@@ -0,0 +1,102 @@
'use client';
interface PaginationProps {
totalPages: number;
currentPage: number;
onPageChange: (page: number) => void;
}
export default function ClientPagination({ totalPages, currentPage, onPageChange }: PaginationProps) {
if (totalPages <= 1) return null;
const getPaginationItems = () => {
const siblingCount = 2;
const totalPageNumbers = siblingCount * 2 + 3;
if (totalPages <= totalPageNumbers) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);
const showLeftDots = leftSiblingIndex > 2;
const showRightDots = rightSiblingIndex < totalPages - 1;
if (!showLeftDots && showRightDots) {
const leftItemCount = 3 + 2 * siblingCount;
const leftRange = Array.from({ length: leftItemCount }, (_, i) => i + 1);
return [...leftRange, '...', totalPages];
}
if (showLeftDots && !showRightDots) {
const rightItemCount = 3 + 2 * siblingCount;
const rightRange = Array.from(
{ length: rightItemCount },
(_, i) => totalPages - rightItemCount + i + 1
);
return [1, '...', ...rightRange];
}
if (showLeftDots && showRightDots) {
const middleRange = Array.from(
{ length: rightSiblingIndex - leftSiblingIndex + 1 },
(_, i) => leftSiblingIndex + i
);
return [1, '...', ...middleRange, '...', totalPages];
}
return [];
};
const pages = getPaginationItems();
return (
<div className="flex justify-center items-center gap-2 mt-10 mb-6 flex-wrap" dir="rtl">
{/* دکمه قبلی */}
<button
onClick={() => onPageChange(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"
>
قبلی
</button>
{/* شماره صفحات */}
<div className="flex items-center gap-1.5 md:gap-2">
{pages.map((page, index) => {
if (page === '...') {
return (
<span key={`dots-${index}`} className="px-1 md:px-2 py-2 text-gray-400 font-bold tracking-widest">
...
</span>
);
}
return (
<button
key={index}
onClick={() => onPageChange(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' // استایل دکمه فعال (زرد)
: 'bg-[#1A2332] text-gray-300 hover:bg-[#1A2332]/80 hover:text-white' // استایل دکمه‌های عادی (تیره)
}`}
>
{page}
</button>
);
})}
</div>
{/* دکمه بعدی */}
<button
onClick={() => onPageChange(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"
>
بعدی
</button>
</div>
);
}

View File

@@ -0,0 +1,71 @@
// 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

@@ -66,8 +66,8 @@ export default function ProductCard({ product }: ProductCardProps) {
*/} */}
{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>{product.attributes[0].name}:</p> {/* 👈 نام attribute اول */} <p className="text-[0.9em]">{product.attributes[0].name}:</p> {/* 👈 نام attribute اول */}
<p className="font-semibold 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> {/* 👈 مقدار attribute اول */}
</div> </div>
)} )}
@@ -76,8 +76,8 @@ export default function ProductCard({ product }: ProductCardProps) {
*/} */}
{product.attributes?.[1] && ( {product.attributes?.[1] && (
<div className="flex justify-between"> <div className="flex justify-between">
<p>{product.attributes[1].name}:</p> {/* 👈 نام attribute دوم */} <p className="text-[0.9em]">{product.attributes[1].name}:</p> {/* 👈 نام attribute دوم */}
<p className="font-semibold 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> {/* 👈 مقدار attribute دوم */}
</div> </div>
)} )}
</div> </div>

View File

@@ -0,0 +1,27 @@
import { API_BASE_URL } from "../config";
export async function getAllBrands() {
try {
const res = await fetch(`${API_BASE_URL}/products/brands?page=1&limit=50`, { cache: 'no-store' });
if (!res.ok) return [];
const json = await res.json();
return json.data?.items || [];
} catch (error) {
return [];
}
}
export async function getBrandFilters(slug: string) {
try {
const res = await fetch(`${API_BASE_URL}/products/brands/${slug}/filters?page=1&limit=20`, {
cache: 'no-store'
});
if (!res.ok) throw new Error('Failed to fetch brand filters');
const json = await res.json();
return json.data;
} catch (error) {
console.error("Error fetching brand filters:", error);
return null;
}
}

View File

@@ -16,3 +16,17 @@ export const categoryService = {
} }
}, },
}; };
export async function getCategoryFilters(slug: string) {
try {
const res = await fetch(`${API_BASE_URL}/products/categories/${slug}/filters?page=1&limit=20`, {
cache: 'no-store' // برای دریافت همیشه دیتای تازه
});
if (!res.ok) throw new Error('Failed to fetch category filters');
const json = await res.json();
return json.data;
} catch (error) {
console.error("Error fetching category filters:", error);
return null;
}
}

View File

@@ -33,6 +33,7 @@ export async function getProductsByCategory(
} }
} }
export async function getProducts(page = 1, limit = 20) { export async function getProducts(page = 1, limit = 20) {
try { try {
const url = `${API_BASE_URL}/products?page=${page}&limit=${limit}`; const url = `${API_BASE_URL}/products?page=${page}&limit=${limit}`;