pagination/filter product api
This commit is contained in:
@@ -9,9 +9,7 @@ interface PageProps {
|
||||
}
|
||||
|
||||
export default async function SingleProductPage({ params }: PageProps) {
|
||||
// --------------------------------------------------------
|
||||
// ۱. منطق دریافت دادهها (دقیقا مشابه درخواست شما)
|
||||
// --------------------------------------------------------
|
||||
|
||||
const resolvedParams = await params;
|
||||
const slug = resolvedParams.slug;
|
||||
|
||||
@@ -28,10 +26,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// ۲. آمادهسازی و مپ کردن دادههای API برای UI
|
||||
// --------------------------------------------------------
|
||||
|
||||
|
||||
// فرمت کردن قیمت
|
||||
const formattedPrice = product.display_price
|
||||
? product.display_price.toLocaleString('fa-IR')
|
||||
@@ -49,12 +44,10 @@ export default async function SingleProductPage({ params }: PageProps) {
|
||||
return attr ? attr.valueText : "-";
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
// ۳. خروجی UI (بدون تغییر در ساختار و استایلها)
|
||||
// --------------------------------------------------------
|
||||
|
||||
return (
|
||||
<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 />
|
||||
|
||||
{/* مسیر راهنما */}
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
// app/products/page.tsx
|
||||
import ProductCard from '@/components/productcard';
|
||||
import { getProducts } from '@/public/src/services/products/api';
|
||||
import ProductGrid from '@/components/clientProduct';
|
||||
|
||||
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,
|
||||
title: p.title,
|
||||
brand: p.brand,
|
||||
price: p.calculated_price,
|
||||
stock: p.stock,
|
||||
slug: p.slug,
|
||||
image: p.mainImageUrl || "/placeholder.png",
|
||||
image: p.mainImageUrl || "/src/img/1.jpg",
|
||||
attributes: p.attributes
|
||||
}));
|
||||
})) || [];
|
||||
|
||||
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>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{products.map((product: any) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
{/* پاس دادن کل محصولات به کامپوننت کلاینت */}
|
||||
<ProductGrid products={products} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
27
public/src/services/brand/api.tsx
Normal file
27
public/src/services/brand/api.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export async function getProductsByCategory(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getProducts(page = 1, limit = 20) {
|
||||
try {
|
||||
const url = `${API_BASE_URL}/products?page=${page}&limit=${limit}`;
|
||||
|
||||
Reference in New Issue
Block a user