From 1e59e865f2f00394e88b4b2aa99978234622f0db Mon Sep 17 00:00:00 2001 From: Stephanie Dietz <49788645+StephDietz@users.noreply.github.com> Date: Mon, 9 Oct 2023 04:43:26 -0500 Subject: [PATCH 1/6] Truncate pagination (#200) * fix pagination bug * fix hover bug on active page --- .../15-final/app/ui/invoices/pagination.tsx | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/dashboard/15-final/app/ui/invoices/pagination.tsx b/dashboard/15-final/app/ui/invoices/pagination.tsx index 044a854..5af0925 100644 --- a/dashboard/15-final/app/ui/invoices/pagination.tsx +++ b/dashboard/15-final/app/ui/invoices/pagination.tsx @@ -19,18 +19,38 @@ export default function Pagination({ const PreviousPageTag = Link; const NextPageTag = Link; - const createPageUrl = (pageNumber: number) => { + 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, + ]; + } + return (
- {allPages.map((page, i) => { - const PageTag = page === currentPage ? 'p' : Link; + {pagesToShow.map((page, i) => { + if (page === '...') { + return ( + + ... + + ); + } + + const PageTag = page === currentPage || page === '...' ? 'p' : Link; return ( Date: Mon, 9 Oct 2023 16:59:24 +0100 Subject: [PATCH 2/6] 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 --- .../15-final/app/dashboard/customers/page.tsx | 10 +- .../15-final/app/dashboard/invoices/page.tsx | 12 +- dashboard/15-final/app/lib/utils.ts | 33 ++++ .../15-final/app/ui/invoices/pagination.tsx | 171 ++++++++++-------- dashboard/15-final/app/ui/search.tsx | 2 + 5 files changed, 140 insertions(+), 88 deletions(-) diff --git a/dashboard/15-final/app/dashboard/customers/page.tsx b/dashboard/15-final/app/dashboard/customers/page.tsx index c45ee90..dcd62af 100644 --- a/dashboard/15-final/app/dashboard/customers/page.tsx +++ b/dashboard/15-final/app/dashboard/customers/page.tsx @@ -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 || ''; diff --git a/dashboard/15-final/app/dashboard/invoices/page.tsx b/dashboard/15-final/app/dashboard/invoices/page.tsx index 11bfce8..fcf178a 100644 --- a/dashboard/15-final/app/dashboard/invoices/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/page.tsx @@ -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, diff --git a/dashboard/15-final/app/lib/utils.ts b/dashboard/15-final/app/lib/utils.ts index 4dce958..b7f7cff 100644 --- a/dashboard/15-final/app/lib/utils.ts +++ b/dashboard/15-final/app/lib/utils.ts @@ -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, + ]; +}; diff --git a/dashboard/15-final/app/ui/invoices/pagination.tsx b/dashboard/15-final/app/ui/invoices/pagination.tsx index 5af0925..a9c6dba 100644 --- a/dashboard/15-final/app/ui/invoices/pagination.tsx +++ b/dashboard/15-final/app/ui/invoices/pagination.tsx @@ -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 (
- - - -
- {pagesToShow.map((page, i) => { - if (page === '...') { - return ( - - ... - - ); - } + + +
+ {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 ( - - {page} - + href={createPageURL(page)} + page={page} + position={position} + isActive={currentPage === page} + /> ); })}
- - - + + = totalPages} + />
); } + +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' ? ( +
{page}
+ ) : ( + + {page} + + ); +} + +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' ? ( + + ) : ( + + ); + + return isDisabled ? ( +
{icon}
+ ) : ( + + {icon} + + ); +} diff --git a/dashboard/15-final/app/ui/search.tsx b/dashboard/15-final/app/ui/search.tsx index bb58994..7c89054 100644 --- a/dashboard/15-final/app/ui/search.tsx +++ b/dashboard/15-final/app/ui/search.tsx @@ -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 { From c6413f9037087e8f9e0b0901c16dcc45d7c940bc Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:04:46 +0100 Subject: [PATCH 3/6] Add table loading skeletons and move data fetching down (#207) * Use tailwind variants * Move data fetching down * Add table skeleton * Update pagination data fetching * Fix skeleton layout shift * Add key to suspense to trigger it on subsequent navigation --- .../15-final/app/dashboard/invoices/page.tsx | 18 +- dashboard/15-final/app/lib/data.ts | 49 ++-- .../15-final/app/ui/dashboard/skeletons.tsx | 115 +++++++++ .../15-final/app/ui/invoices/pagination.tsx | 12 +- dashboard/15-final/app/ui/invoices/table.tsx | 221 ++++++++---------- 5 files changed, 255 insertions(+), 160 deletions(-) diff --git a/dashboard/15-final/app/dashboard/invoices/page.tsx b/dashboard/15-final/app/dashboard/invoices/page.tsx index fcf178a..fd35c3f 100644 --- a/dashboard/15-final/app/dashboard/invoices/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/page.tsx @@ -2,8 +2,10 @@ import Pagination from '@/app/ui/invoices/pagination'; import Search from '@/app/ui/search'; import { CreateInvoice } from '@/app/ui/invoices/buttons'; import Table from '@/app/ui/invoices/table'; -import { fetchFilteredInvoices } from '@/app/lib/data'; import { lusitana } from '@/app/ui/fonts'; +import { InvoicesTableSkeleton } from '@/app/ui/dashboard/skeletons'; +import { Suspense } from 'react'; +import { fetchInvoicesPages } from '@/app/lib/data'; export default async function Page({ searchParams, @@ -14,12 +16,8 @@ export default async function Page({ }; }) { const query = searchParams?.query || ''; - const currentPage = Number(searchParams?.page || '1'); - - const { invoices, totalPages } = await fetchFilteredInvoices( - query, - currentPage, - ); + const currentPage = Number(searchParams?.page) || 1; + const totalPages = await fetchInvoicesPages(query); return (
@@ -30,9 +28,11 @@ export default async function Page({
- + }> +
+
- +
); diff --git a/dashboard/15-final/app/lib/data.ts b/dashboard/15-final/app/lib/data.ts index 61b94a0..b5e75d5 100644 --- a/dashboard/15-final/app/lib/data.ts +++ b/dashboard/15-final/app/lib/data.ts @@ -79,15 +79,15 @@ export async function fetchCardData() { } } +const ITEMS_PER_PAGE = 6; export async function fetchFilteredInvoices( query: string, currentPage: number, ) { - const itemsPerPage = 6; - const offset = (currentPage - 1) * itemsPerPage; + const offset = (currentPage - 1) * ITEMS_PER_PAGE; try { - const data = await sql` + const invoices = await sql` SELECT invoices.id, invoices.amount, @@ -105,34 +105,37 @@ export async function fetchFilteredInvoices( invoices.date::text ILIKE ${`%${query}%`} OR invoices.status ILIKE ${`%${query}%`} ORDER BY invoices.date DESC - LIMIT ${itemsPerPage} OFFSET ${offset} + LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset} `; - const count = await sql` - SELECT COUNT(*) - FROM invoices - JOIN customers ON invoices.customer_id = customers.id - WHERE - customers.name ILIKE ${`%${query}%`} OR - customers.email ILIKE ${`%${query}%`} OR - invoices.amount::text ILIKE ${`%${query}%`} OR - invoices.date::text ILIKE ${`%${query}%`} OR - invoices.status ILIKE ${`%${query}%`} - `; - - const totalRecords = Number(count.rows[0].count); - const totalPages = Math.ceil(totalRecords / itemsPerPage); - - return { - invoices: data.rows, - totalPages, - }; + return invoices.rows; } catch (error) { console.error('Database Error:', error); throw new Error('Failed to fetch invoices.'); } } +export async function fetchInvoicesPages(query: string) { + try { + const count = await sql`SELECT COUNT(*) + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + WHERE + customers.name ILIKE ${`%${query}%`} OR + customers.email ILIKE ${`%${query}%`} OR + invoices.amount::text ILIKE ${`%${query}%`} OR + invoices.date::text ILIKE ${`%${query}%`} OR + invoices.status ILIKE ${`%${query}%`} + `; + + const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE); + return totalPages; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch total number of invoices.'); + } +} + export async function fetchInvoiceById(id: string) { try { const data = await sql` diff --git a/dashboard/15-final/app/ui/dashboard/skeletons.tsx b/dashboard/15-final/app/ui/dashboard/skeletons.tsx index cdab3ba..c57322d 100644 --- a/dashboard/15-final/app/ui/dashboard/skeletons.tsx +++ b/dashboard/15-final/app/ui/dashboard/skeletons.tsx @@ -90,3 +90,118 @@ export default function DashboardSkeleton() { ); } + +export function TableRowSkeleton() { + return ( + + {/* Customer Name and Image */} + + {/* Email */} + + {/* Amount */} + + {/* Date */} + + {/* Status */} + + {/* Actions */} + + + ); +} + +export function InvoicesMobileSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +export function InvoicesTableSkeleton() { + return ( +
+
+
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ Customer + + Email + + Amount + + Date + + Status + + Edit +
+
+
+
+ ); +} diff --git a/dashboard/15-final/app/ui/invoices/pagination.tsx b/dashboard/15-final/app/ui/invoices/pagination.tsx index a9c6dba..e9d0293 100644 --- a/dashboard/15-final/app/ui/invoices/pagination.tsx +++ b/dashboard/15-final/app/ui/invoices/pagination.tsx @@ -6,15 +6,11 @@ import Link from 'next/link'; import { generatePagination } from '@/app/lib/utils'; import { usePathname, useSearchParams } from 'next/navigation'; -export default function Pagination({ - currentPage, - totalPages, -}: { - currentPage: number; - totalPages: number; -}) { +export default function Pagination({ totalPages }: { totalPages: number }) { const pathname = usePathname(); const searchParams = useSearchParams(); + const currentPage = Number(searchParams.get('page')) || 1; + const allPages = generatePagination(currentPage, totalPages); const createPageURL = (pageNumber: number | string) => { const params = new URLSearchParams(searchParams); @@ -22,8 +18,6 @@ export default function Pagination({ return `${pathname}?${params.toString()}`; }; - const allPages = generatePagination(currentPage, totalPages); - return (
- -
-
-
-
- {invoices?.map((invoice) => ( -
-
-
-
- {`${invoice.name}'s -

{invoice.name}

-
-

{invoice.email}

+
+
+
+ {invoices?.map((invoice) => ( +
+
+
+
+ {`${invoice.name}'s +

{invoice.name}

- +

{invoice.email}

-
-
-

- {formatCurrency(invoice.amount)} -

-

{formatDateToLocal(invoice.date)}

-
-
- - -
+ +
+
+
+

+ {formatCurrency(invoice.amount)} +

+

{formatDateToLocal(invoice.date)}

+
+
+ +
- ))} -
- - - - - - - - - - - - - {invoices?.map((invoice) => ( - - - - - - - + + ))} + +
- Customer - - Email - - Amount - - Date - - Status - - Edit -
-
- {`${invoice.name}'s -

{invoice.name}

-
-
- {invoice.email} - - {formatCurrency(invoice.amount)} - - {formatDateToLocal(invoice.date)} - - - + + ))} + + + + + + + + + + + + + + {invoices?.map((invoice) => ( + + + + + + + - - ))} - -
+ Customer + + Email + + Amount + + Date + + Status + + Edit +
+
+ {`${invoice.name}'s +

{invoice.name}

+
+
+ {invoice.email} + + {formatCurrency(invoice.amount)} + + {formatDateToLocal(invoice.date)} + + + +
-
- + +
From 3d5c36a6e66ae16a0595dd9fc1662ef398b8251b Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:21:37 +0100 Subject: [PATCH 4/6] Move breadcrumbs from
up to the (#208) * Move breadcrumbs up to page * Use default exports * Remove extra divs --- .../app/dashboard/invoices/[id]/edit/page.tsx | 11 + .../app/dashboard/invoices/create/page.tsx | 11 + .../15-final/app/dashboard/invoices/page.tsx | 3 +- .../15-final/app/ui/invoices/breadcrumbs.tsx | 6 +- .../15-final/app/ui/invoices/create-form.tsx | 292 ++++++++--------- .../15-final/app/ui/invoices/edit-form.tsx | 300 ++++++++---------- .../15-final/app/ui/invoices/pagination.tsx | 3 +- dashboard/15-final/app/ui/search.tsx | 1 + 8 files changed, 311 insertions(+), 316 deletions(-) diff --git a/dashboard/15-final/app/dashboard/invoices/[id]/edit/page.tsx b/dashboard/15-final/app/dashboard/invoices/[id]/edit/page.tsx index c02654e..3529961 100644 --- a/dashboard/15-final/app/dashboard/invoices/[id]/edit/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/[id]/edit/page.tsx @@ -1,6 +1,7 @@ import { fetchInvoiceById, fetchCustomerNames } from '@/app/lib/data'; import { notFound } from 'next/navigation'; import Form from '@/app/ui/invoices/edit-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; export default async function Page({ params }: { params: { id: string } }) { const id = params.id; @@ -13,6 +14,16 @@ export default async function Page({ params }: { params: { id: string } }) { return (
+
); diff --git a/dashboard/15-final/app/dashboard/invoices/create/page.tsx b/dashboard/15-final/app/dashboard/invoices/create/page.tsx index b50595f..3d036cc 100644 --- a/dashboard/15-final/app/dashboard/invoices/create/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/create/page.tsx @@ -1,11 +1,22 @@ import { fetchCustomerNames } from '@/app/lib/data'; import Form from '@/app/ui/invoices/create-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; export default async function Page() { const customerNames = await fetchCustomerNames(); return (
+
); diff --git a/dashboard/15-final/app/dashboard/invoices/page.tsx b/dashboard/15-final/app/dashboard/invoices/page.tsx index fd35c3f..1f4dca1 100644 --- a/dashboard/15-final/app/dashboard/invoices/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/page.tsx @@ -1,7 +1,7 @@ import Pagination from '@/app/ui/invoices/pagination'; import Search from '@/app/ui/search'; -import { CreateInvoice } from '@/app/ui/invoices/buttons'; import Table from '@/app/ui/invoices/table'; +import { CreateInvoice } from '@/app/ui/invoices/buttons'; import { lusitana } from '@/app/ui/fonts'; import { InvoicesTableSkeleton } from '@/app/ui/dashboard/skeletons'; import { Suspense } from 'react'; @@ -17,6 +17,7 @@ export default async function Page({ }) { const query = searchParams?.query || ''; const currentPage = Number(searchParams?.page) || 1; + const totalPages = await fetchInvoicesPages(query); return ( diff --git a/dashboard/15-final/app/ui/invoices/breadcrumbs.tsx b/dashboard/15-final/app/ui/invoices/breadcrumbs.tsx index 50779a6..cd9e226 100644 --- a/dashboard/15-final/app/ui/invoices/breadcrumbs.tsx +++ b/dashboard/15-final/app/ui/invoices/breadcrumbs.tsx @@ -8,7 +8,11 @@ interface Breadcrumb { active?: boolean; } -export function Breadcrumbs({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) { +export default function Breadcrumbs({ + breadcrumbs, +}: { + breadcrumbs: Breadcrumb[]; +}) { return (