pagination/filter product api
This commit is contained in:
64
components/clientProduct.tsx
Normal file
64
components/clientProduct.tsx
Normal 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
102
components/pagination.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
71
components/productFilters.tsx
Normal file
71
components/productFilters.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -66,8 +66,8 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
*/}
|
||||
{product.attributes?.[0] && (
|
||||
<div className="flex border-b border-[#dfdfdf] mb-2 pb-2 justify-between">
|
||||
<p>{product.attributes[0].name}:</p> {/* 👈 نام attribute اول */}
|
||||
<p className="font-semibold text-black">{product.attributes[0].valueText || "-"}</p> {/* 👈 مقدار attribute اول */}
|
||||
<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 اول */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -76,8 +76,8 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
*/}
|
||||
{product.attributes?.[1] && (
|
||||
<div className="flex justify-between">
|
||||
<p>{product.attributes[1].name}:</p> {/* 👈 نام attribute دوم */}
|
||||
<p className="font-semibold text-black">{product.attributes[1].valueText || "-"}</p> {/* 👈 مقدار attribute دوم */}
|
||||
<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 دوم */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user