Skip to content

Commit

Permalink
add pagination and search bar to the invoice table (#146)
Browse files Browse the repository at this point in the history
* add pagination and search bar to the invoice table

* make table component server

* fix eslint

* eslint

* eslint

* Updates pagination url to not use state or router and moves pagination component up above the table

* fix eslint typing error

* resolve github convos

* update all 'q' to be 'query'

---------

Co-authored-by: Michael Novotny <manovotny@gmail.com>
  • Loading branch information
StephDietz and manovotny authored Sep 7, 2023
1 parent a7063b9 commit b8935ab
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 5 deletions.
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 @@ import {
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 @@ function renderInvoiceStatus(status: string) {
}
}

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 @@ export default function InvoicesTable() {
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,7 +118,7 @@ export default function InvoicesTable() {
</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}
Expand Down

0 comments on commit b8935ab

Please sign in to comment.