mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-26 06:06:03 +00:00
Fix pagination bugs and refactor/abstract styling logic (#205)
* Simplify TS * Abstract away pagination logic * Clean up * Fix bugs and refactor styling logic * Add comments * Fix border bug for single page * Fix search pagination
This commit is contained in:
committed by
GitHub
parent
1e59e865f2
commit
728368d712
@@ -4,12 +4,10 @@ import CustomersTable from '@/app/ui/customers/table';
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams:
|
||||
| {
|
||||
query: string | undefined;
|
||||
page: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
searchParams?: {
|
||||
query?: string;
|
||||
page?: string;
|
||||
};
|
||||
}) {
|
||||
const query = searchParams?.query || '';
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ import { lusitana } from '@/app/ui/fonts';
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams:
|
||||
| {
|
||||
query: string | undefined;
|
||||
page: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
searchParams?: {
|
||||
query?: string;
|
||||
page?: string;
|
||||
};
|
||||
}) {
|
||||
const query = searchParams?.query || '';
|
||||
const currentPage = query ? 1 : Number(searchParams?.page || '1');
|
||||
const currentPage = Number(searchParams?.page || '1');
|
||||
|
||||
const { invoices, totalPages } = await fetchFilteredInvoices(
|
||||
query,
|
||||
|
||||
@@ -34,3 +34,36 @@ export const generateYAxis = (revenue: Revenue[]) => {
|
||||
|
||||
return { yAxisLabels, topLabel };
|
||||
};
|
||||
|
||||
export const generatePagination = (currentPage: number, totalPages: number) => {
|
||||
// If the total number of pages is 7 or less,
|
||||
// display all pages without any ellipsis.
|
||||
if (totalPages <= 7) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
// If the current page is among the first 3 pages,
|
||||
// show the first 3, an ellipsis, and the last 2 pages.
|
||||
if (currentPage <= 3) {
|
||||
return [1, 2, 3, '...', totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is among the last 3 pages,
|
||||
// show the first 2, an ellipsis, and the last 3 pages.
|
||||
if (currentPage >= totalPages - 2) {
|
||||
return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is somewhere in the middle,
|
||||
// show the first page, an ellipsis, the current page and its neighbors,
|
||||
// another ellipsis, and the last page.
|
||||
return [
|
||||
1,
|
||||
'...',
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
'...',
|
||||
totalPages,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import { generatePagination } from '@/app/lib/utils';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Pagination({
|
||||
currentPage,
|
||||
@@ -15,94 +16,114 @@ export default function Pagination({
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const allPages = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
const PreviousPageTag = Link;
|
||||
const NextPageTag = Link;
|
||||
|
||||
const createPageUrl = (pageNumber: number | string) => {
|
||||
const createPageURL = (pageNumber: number | string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('page', pageNumber.toString());
|
||||
return `${pathname}?${params.toString()}`;
|
||||
};
|
||||
|
||||
let pagesToShow = [];
|
||||
|
||||
if (totalPages <= 7) {
|
||||
pagesToShow = allPages;
|
||||
} else if (currentPage <= 3) {
|
||||
pagesToShow = [1, 2, 3, '...', totalPages - 1, totalPages];
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pagesToShow = [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
|
||||
} else {
|
||||
pagesToShow = [
|
||||
1,
|
||||
'...',
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
'...',
|
||||
totalPages,
|
||||
];
|
||||
}
|
||||
const allPages = generatePagination(currentPage, totalPages);
|
||||
|
||||
return (
|
||||
<div className="inline-flex">
|
||||
<PreviousPageTag
|
||||
href={createPageUrl(currentPage - 1)}
|
||||
className={clsx(
|
||||
'mr-2 flex h-10 w-10 items-center justify-center rounded-md ring-1 ring-inset ring-gray-300 hover:bg-gray-100 md:mr-4',
|
||||
{
|
||||
'pointer-events-none text-gray-300 hover:bg-transparent':
|
||||
currentPage === 1,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4" />
|
||||
</PreviousPageTag>
|
||||
<div className="flex -space-x-px ">
|
||||
{pagesToShow.map((page, i) => {
|
||||
if (page === '...') {
|
||||
return (
|
||||
<span
|
||||
key={`ellipsis-${i}`}
|
||||
className="flex h-10 w-10 items-center justify-center text-sm ring-1 ring-inset ring-gray-300"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
<PaginationArrow
|
||||
direction="left"
|
||||
href={createPageURL(currentPage - 1)}
|
||||
isDisabled={currentPage <= 1}
|
||||
/>
|
||||
|
||||
<div className="flex -space-x-px">
|
||||
{allPages.map((page, index) => {
|
||||
let position: 'first' | 'last' | 'single' | 'middle' | undefined;
|
||||
|
||||
if (index === 0) position = 'first';
|
||||
if (index === allPages.length - 1) position = 'last';
|
||||
if (allPages.length === 1) position = 'single';
|
||||
if (page === '...') position = 'middle';
|
||||
|
||||
const PageTag = page === currentPage || page === '...' ? 'p' : Link;
|
||||
return (
|
||||
<PageTag
|
||||
<PaginationNumber
|
||||
key={page}
|
||||
href={createPageUrl(page)}
|
||||
className={clsx(
|
||||
i === 0 && 'rounded-l-md',
|
||||
i === pagesToShow.length - 1 && 'rounded-r-md',
|
||||
'flex h-10 w-10 items-center justify-center text-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100',
|
||||
{
|
||||
'z-10 bg-blue-600 text-white ring-blue-600 hover:bg-blue-600':
|
||||
currentPage === page,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{page}
|
||||
</PageTag>
|
||||
href={createPageURL(page)}
|
||||
page={page}
|
||||
position={position}
|
||||
isActive={currentPage === page}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<NextPageTag
|
||||
href={createPageUrl(currentPage + 1)}
|
||||
className={clsx(
|
||||
'ml-2 flex h-10 w-10 items-center justify-center rounded-md ring-1 ring-inset ring-gray-300 hover:bg-gray-100 md:ml-4',
|
||||
{
|
||||
'text-gray-300': currentPage === totalPages,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ArrowRightIcon className="w-4" />
|
||||
</NextPageTag>
|
||||
|
||||
<PaginationArrow
|
||||
direction="right"
|
||||
href={createPageURL(currentPage + 1)}
|
||||
isDisabled={currentPage >= totalPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNumber({
|
||||
page,
|
||||
href,
|
||||
isActive,
|
||||
position,
|
||||
}: {
|
||||
page: number | string;
|
||||
href: string;
|
||||
position?: 'first' | 'last' | 'middle' | 'single';
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const className = clsx(
|
||||
'flex h-10 w-10 items-center justify-center text-sm border',
|
||||
{
|
||||
'rounded-l-md': position === 'first' || position === 'single',
|
||||
'rounded-r-md': position === 'last' || position === 'single',
|
||||
'z-10 bg-blue-600 border-blue-600 text-white': isActive,
|
||||
'hover:bg-gray-100': !isActive && position !== 'middle',
|
||||
'text-gray-300': position === 'middle',
|
||||
},
|
||||
);
|
||||
|
||||
return isActive || position === 'middle' ? (
|
||||
<div className={className}>{page}</div>
|
||||
) : (
|
||||
<Link href={href} className={className}>
|
||||
{page}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationArrow({
|
||||
href,
|
||||
direction,
|
||||
isDisabled,
|
||||
}: {
|
||||
href: string;
|
||||
direction: 'left' | 'right';
|
||||
isDisabled?: boolean;
|
||||
}) {
|
||||
const className = clsx(
|
||||
'flex h-10 w-10 items-center justify-center rounded-md border',
|
||||
{
|
||||
'pointer-events-none text-gray-300': isDisabled,
|
||||
'hover:bg-gray-100': !isDisabled,
|
||||
'mr-2 md:mr-4': direction === 'left',
|
||||
'ml-2 md:ml-4': direction === 'right',
|
||||
},
|
||||
);
|
||||
|
||||
const icon =
|
||||
direction === 'left' ? (
|
||||
<ArrowLeftIcon className="w-4" />
|
||||
) : (
|
||||
<ArrowRightIcon className="w-4" />
|
||||
);
|
||||
|
||||
return isDisabled ? (
|
||||
<div className={className}>{icon}</div>
|
||||
) : (
|
||||
<Link className={className} href={href}>
|
||||
{icon}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export default function Search({ placeholder }: { placeholder: string }) {
|
||||
console.log(`Searching... ${term}`);
|
||||
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
params.set('page', '1');
|
||||
if (term) {
|
||||
params.set('query', term);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user