Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pagination and search bar to the invoice table #146

Merged
merged 11 commits into from
Sep 7, 2023
Merged
11 changes: 9 additions & 2 deletions dashboard/15-final/app/dashboard/invoices/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import InvoicesTable from '@/app/ui/invoices/table';

export default function Page() {
export default function Page({
searchParams,
}: {
searchParams: {
query: string;
page: string;
};
}) {
return (
<div>
<InvoicesTable />
<InvoicesTable searchParams={searchParams} />
</div>
);
}
49 changes: 49 additions & 0 deletions dashboard/15-final/app/lib/dummy-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,55 @@ export const invoices: Invoice[] = [
status: 'paid',
date: '2023-06-01',
},
{
id: 9,
customerId: 3,
amount: 1250,
status: 'paid',
date: '2023-06-02',
},
{
id: 10,
customerId: 1,
amount: 8945,
status: 'paid',
date: '2023-06-01',
},
{
id: 11,
customerId: 2,
amount: 500,
status: 'paid',
date: '2023-08-01',
},
{
id: 12,
customerId: 3,
amount: 8945,
status: 'paid',
date: '2023-06-01',
},
{
id: 13,
customerId: 3,
amount: 8945,
status: 'paid',
date: '2023-06-01',
},
{
id: 14,
customerId: 4,
amount: 8945,
status: 'paid',
date: '2023-10-01',
},
{
id: 15,
customerId: 3,
amount: 1000,
status: 'paid',
date: '2022-06-12',
},
];

export const revenue: Revenue[] = [
Expand Down
71 changes: 71 additions & 0 deletions dashboard/15-final/app/ui/invoices/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client';

import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { usePathname, useSearchParams } from 'next/navigation';
import clsx from 'clsx';
import Link from 'next/link';

export default function PaginationButtons({
totalPages,
currentPage,
}: {
totalPages: number;
currentPage: number;
}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);

const createPageUrl = (pageNumber: number) => {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.set('page', pageNumber.toString());
return `${pathname}?${newSearchParams.toString()}`;
};

const PreviousPageTag = currentPage === 1 ? 'p' : Link;
const NextPageTag = currentPage === totalPages ? 'p' : Link;

return (
<div className="flex items-center justify-end">
<PreviousPageTag
href={createPageUrl(currentPage - 1)}
className={clsx(
'flex h-8 w-8 items-center justify-center rounded-l-md border border-gray-300',
{
'text-gray-300': currentPage === 1,
},
)}
>
<ChevronLeftIcon className="w-4" />
</PreviousPageTag>
{pageNumbers.map((page) => {
const PageTag = page === currentPage ? 'p' : Link;
return (
<PageTag
key={page}
href={createPageUrl(page)}
className={clsx(
'flex h-8 w-8 items-center justify-center border-y border-r border-gray-300 text-sm',
{
'border-blue-600 bg-blue-600 text-white': currentPage === page,
},
)}
>
{page}
</PageTag>
);
})}
<NextPageTag
href={createPageUrl(currentPage + 1)}
className={clsx(
'flex h-8 w-8 items-center justify-center rounded-r-md border border-l-0 border-gray-300',
{
'text-gray-300': currentPage === totalPages,
},
)}
>
<ChevronRightIcon className="w-4" />
</NextPageTag>
</div>
);
}
70 changes: 70 additions & 0 deletions dashboard/15-final/app/ui/invoices/table-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useTransition } from 'react';

export default function TableSearch() {
const { replace } = useRouter();
const searchParams = useSearchParams()!;
const pathname = usePathname();
const [isPending, startTransition] = useTransition();

function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}

startTransition(() => {
replace(`${pathname}?${params.toString()}`);
});
}

return (
<div className="relative max-w-md flex-grow">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative flex items-center px-2 py-2">
<MagnifyingGlassIcon className="h-5 text-gray-400" />
<input
type="text"
placeholder="Search..."
onChange={(e) => handleSearch(e.target.value)}
className="absolute inset-0 w-full rounded-md border border-gray-300 bg-transparent p-2 pl-8 text-sm"
/>
</div>
{isPending && <LoadingIcon />}
</div>
);
}

function LoadingIcon() {
return (
<div className="absolute bottom-0 right-0 top-0 flex items-center justify-center">
<svg
className="-ml-1 mr-3 h-5 w-5 animate-spin text-gray-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
);
}
49 changes: 46 additions & 3 deletions dashboard/15-final/app/ui/invoices/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
CheckCircleIcon,
} from '@heroicons/react/24/outline';
import DeleteInvoice from '@/app/ui/invoices/delete-invoice-button';
import TableSearch from './table-search';
import PaginationButtons from './pagination';

const ITEMS_PER_PAGE = 10;

function renderInvoiceStatus(status: string) {
if (status === 'pending') {
Expand All @@ -27,12 +31,47 @@
}
}

export default function InvoicesTable() {
export default function InvoicesTable({
searchParams,
}: {
searchParams: {
query: string;
page: string;
};
}) {
const searchTerm = searchParams.query ?? '';
const currentPage = parseInt(searchParams.page ?? '1');

const filteredInvoices = invoices.filter((invoice) => {
const customer = getCustomerById(invoice.customerId);

const invoiceMatches = Object.values(invoice).some(
(value) =>
value?.toString().toLowerCase().includes(searchTerm.toLowerCase()),
);

const customerMatches =
customer &&
Object.values(customer).some(
(value) =>
value?.toString().toLowerCase().includes(searchTerm.toLowerCase()),
);

return invoiceMatches || customerMatches;
});

const paginatedInvoices = filteredInvoices.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE,
);

function getCustomerById(customerId: number): Customer | null {
const customer = customers.find((customer) => customer.id === customerId);
return customer ? customer : null;
}

const totalPages = Math.ceil(filteredInvoices.length / ITEMS_PER_PAGE);

return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
Expand All @@ -44,7 +83,11 @@
Add Invoice
</Link>
</div>
<div className="mt-8">
<div className="mt-8 flex items-center justify-between">
<TableSearch />
<PaginationButtons totalPages={totalPages} currentPage={currentPage} />
</div>
<div className="mt-4">
<div className="overflow-x-auto">
<div className="overflow-hidden rounded-md border">
<table className="min-w-full divide-y divide-gray-300">
Expand Down Expand Up @@ -75,14 +118,14 @@
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-500">
{invoices.map((invoice) => (
{paginatedInvoices.map((invoice) => (
<tr key={invoice.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
{invoice.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<div className="flex items-center gap-3">
<img

Check warning on line 128 in dashboard/15-final/app/ui/invoices/table.tsx

View workflow job for this annotation

GitHub Actions / test

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={getCustomerById(invoice.customerId)?.imageUrl}
className="h-7 w-7 rounded-full"
/>
Expand Down
Loading