pagination/filter product api
This commit is contained in:
@@ -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,9 +26,6 @@ export default async function SingleProductPage({ params }: PageProps) {
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// ۲. آمادهسازی و مپ کردن دادههای API برای UI
|
|
||||||
// --------------------------------------------------------
|
|
||||||
|
|
||||||
// فرمت کردن قیمت
|
// فرمت کردن قیمت
|
||||||
const formattedPrice = product.display_price
|
const formattedPrice = product.display_price
|
||||||
@@ -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 />
|
||||||
|
|
||||||
{/* مسیر راهنما */}
|
{/* مسیر راهنما */}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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] && (
|
{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>
|
||||||
|
|||||||
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) {
|
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}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user