Update the UI for the invoices page. (#191)

* table ui / invoice page

* add mobile table styling

* move css styles to parent

* resolve comments

* move create button to be inline with search

* remove hey from table lol

* fix awkward mobile scrolling

---------

Co-authored-by: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com>
This commit is contained in:
Stephanie Dietz
2023-10-04 13:37:21 -05:00
committed by GitHub
parent 36a6ef35f6
commit 32a3529efa
8 changed files with 132 additions and 64 deletions

View File

@@ -3,6 +3,7 @@ import Search from '@/app/ui/invoices/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';
export default async function Page({
searchParams,
@@ -25,14 +26,16 @@ export default async function Page({
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className="text-base font-semibold">Invoices</h1>
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search />
<CreateInvoice />
</div>
<div className="mt-8 flex items-center justify-between gap-2">
<Search />
<Table invoices={invoices} />
<div className="mt-5 flex w-full justify-center">
<Pagination totalPages={totalPages} currentPage={currentPage} />
</div>
<Table invoices={invoices} />
</div>
);
}

View File

@@ -6,7 +6,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow overflow-y-auto p-6 md:p-12">{children}</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}

View File

@@ -82,7 +82,7 @@ export async function fetchFilteredInvoices(
query: string,
currentPage: number,
) {
const itemsPerPage = 10;
const itemsPerPage = 6;
const offset = (currentPage - 1) * itemsPerPage;
try {

View File

@@ -1,4 +1,4 @@
import { PencilSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { deleteInvoice } from '@/app/lib/actions';
@@ -6,9 +6,10 @@ export function CreateInvoice() {
return (
<Link
href="/dashboard/invoices/create"
className="block rounded-md bg-blue-500 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
className="flex rounded-md bg-blue-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Create Invoice
<span className="hidden md:block">Create Invoice</span>{' '}
<PlusIcon className="h-5 md:ml-4" />
</Link>
);
}
@@ -17,9 +18,9 @@ export function UpdateInvoice({ id }: { id: string }) {
return (
<Link
href={`/dashboard/invoices/${id}/edit`}
className="rounded-md border p-1"
className="rounded-md border p-2 hover:opacity-60"
>
<PencilSquareIcon className="w-4" />
<PencilIcon className="w-5" />
</Link>
);
}
@@ -28,9 +29,9 @@ export function DeleteInvoice({ id }: { id: string }) {
return (
<form action={deleteInvoice}>
<input type="hidden" name="id" value={id} />
<button className="rounded-md border p-1">
<button className="rounded-md border p-2 hover:opacity-60">
<span className="sr-only">Delete</span>
<TrashIcon className="w-4" />
<TrashIcon className="w-5" />
</button>
</form>
);

View File

@@ -1,6 +1,6 @@
'use client';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import { usePathname, useSearchParams } from 'next/navigation';
import clsx from 'clsx';
import Link from 'next/link';
@@ -26,46 +26,50 @@ export default function Pagination({
};
return (
<div className="inline-flex -space-x-px">
<div className="inline-flex">
<PreviousPageTag
href={createPageUrl(currentPage - 1)}
className={clsx(
'flex h-9 w-9 items-center justify-center rounded-l-md ring-1 ring-inset ring-gray-300',
'mr-4 flex h-10 w-10 items-center justify-center rounded-md ring-1 ring-inset ring-gray-300 hover:opacity-60',
{
'text-gray-300': currentPage === 1,
},
)}
>
<ChevronLeftIcon className="w-4" />
<ArrowLeftIcon className="w-4" />
</PreviousPageTag>
{allPages.map((page) => {
const PageTag = page === currentPage ? 'p' : Link;
return (
<PageTag
key={page}
href={createPageUrl(page)}
className={clsx(
'flex h-9 w-9 items-center justify-center text-sm ring-1 ring-inset ring-gray-300',
{
'z-10 bg-blue-500 text-white ring-blue-500':
currentPage === page,
},
)}
>
{page}
</PageTag>
);
})}
<div className="flex -space-x-px ">
{allPages.map((page, i) => {
const PageTag = page === currentPage ? 'p' : Link;
return (
<PageTag
key={page}
href={createPageUrl(page)}
className={clsx(
i === 0 && 'rounded-l-md',
i === allPages.length - 1 && 'rounded-r-md',
'flex h-10 w-10 items-center justify-center text-sm ring-1 ring-inset ring-gray-300',
{
'z-10 bg-blue-600 text-white ring-blue-600':
currentPage === page,
},
)}
>
{page}
</PageTag>
);
})}
</div>
<NextPageTag
href={createPageUrl(currentPage + 1)}
className={clsx(
'flex h-9 w-9 items-center justify-center rounded-r-md ring-1 ring-inset ring-gray-300',
'ml-4 flex h-10 w-10 items-center justify-center rounded-md ring-1 ring-inset ring-gray-300 hover:opacity-60',
{
'text-gray-300': currentPage === totalPages,
},
)}
>
<ChevronRightIcon className="w-4" />
<ArrowRightIcon className="w-4" />
</NextPageTag>
</div>
);

View File

@@ -22,16 +22,16 @@ export default function Search() {
}, 300);
return (
<div className="relative max-w-md flex-grow">
<div className="min-w-md relative grow md:mr-4">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative flex items-center px-2 py-2">
<div className="relative flex h-10 items-center px-2 py-2">
<MagnifyingGlassIcon className="h-5 text-gray-400" />
<input
type="text"
placeholder="Search..."
className="absolute inset-0 w-full rounded-md border border-gray-300 bg-transparent p-2 pl-8 text-sm"
className="absolute inset-0 h-full w-full rounded-md border border-gray-300 bg-transparent pl-8 text-sm"
onChange={(e) => {
handleSearch(e.target.value);
}}

View File

@@ -1,25 +1,27 @@
import { CheckCircleIcon, ClockIcon } from '@heroicons/react/24/outline';
import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
export default function InvoiceStatus({ status }: { status: string }) {
return (
<span
className={clsx(
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset',
'inline-flex items-center rounded-full px-2 py-1 text-sm',
{
'bg-red-50 text-red-700 ring-red-600/20': status === 'pending',
'bg-green-50 text-green-700 ring-green-600/20': status === 'paid',
'bg-gray-100 text-gray-500': status === 'pending',
'bg-green-500 text-white': status === 'paid',
},
)}
>
{status === 'pending' ? (
<>
<ClockIcon className="mr-1 w-4 text-red-700" /> Pending
Pending
<ClockIcon className="ml-1 w-5 text-gray-500" />
</>
) : null}
{status === 'paid' ? (
<>
<CheckCircleIcon className="mr-1 w-4 text-green-700" /> Paid
Paid
<CheckIcon className="ml-1 w-5 text-white" />
</>
) : null}
</span>

View File

@@ -9,38 +9,96 @@ export default async function InvoicesTable({
}: {
invoices: InvoicesTable[];
}) {
const styles = `
/* Round top-left and top-right corners of the first row in tbody */
tbody tr:first-child td:first-child {
border-top-left-radius: 8px;
}
tbody tr:first-child td:last-child {
border-top-right-radius: 8px;
}
/* Round bottom-left and bottom-right corners of the last row in tbody */
tbody tr:last-child td:first-child {
border-bottom-left-radius: 8px;
}
tbody tr:last-child td:last-child {
border-bottom-right-radius: 8px;
}
`;
return (
<div className="mt-4 flow-root">
<div className="overflow-x-auto">
<style>{styles}</style>
<div>
<div className="inline-block min-w-full align-middle">
<div className="overflow-hidden rounded-md border">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50 text-left text-sm">
<div className="rounded-lg bg-gray-50 p-2">
<div className="md:hidden">
{invoices?.map((invoice) => (
<div
key={invoice.id}
className="mb-2 w-full rounded-md bg-white p-4"
>
<div className="flex items-center justify-between border-b pb-4">
<div>
<div className="mb-2 flex items-center">
<Image
src={invoice.image_url}
className="mr-2 rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`}
/>
<p>{invoice.name}</p>
</div>
<p className="text-sm text-gray-500">{invoice.email}</p>
</div>
<InvoiceStatus status={invoice.status} />
</div>
<div className="flex w-full items-center justify-between pt-4">
<div>
<p className="text-xl font-medium">
{formatCurrency(invoice.amount)}
</p>
<p>{formatDateToLocal(invoice.date)}</p>
</div>
<div className="flex justify-end gap-2">
<UpdateInvoice id={invoice.id} />
<DeleteInvoice id={invoice.id} />
</div>
</div>
</div>
))}
</div>
<table className="hidden min-w-full text-gray-900 md:table">
<thead className="rounded-lg text-left text-sm font-normal">
<tr>
<th scope="col" className="px-6 pr-4 font-semibold">
<th scope="col" className="px-6 pb-4 pt-2">
Customer
</th>
<th scope="col" className="px-3 py-4 font-semibold">
<th scope="col" className="px-3 pb-4 pt-2">
Email
</th>
<th scope="col" className="px-3 py-4 font-semibold">
<th scope="col" className="px-3 pb-4 pt-2">
Amount
</th>
<th scope="col" className="px-3 py-4 font-semibold">
<th scope="col" className="px-3 pb-4 pt-2">
Date
</th>
<th scope="col" className="px-3 py-4 font-semibold">
<th scope="col" className="px-3 pb-4 pt-2">
Status
</th>
<th scope="col" className="relative py-4 pl-3 pr-6 sm:pr-6">
<th
scope="col"
className="relative pb-4 pl-3 pr-6 pt-2 sm:pr-6"
>
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-500">
<tbody className="bg-white">
{invoices?.map((invoice) => (
<tr key={invoice.id}>
<td className="whitespace-nowrap px-6 py-4 text-sm">
<tr key={invoice.id} className="w-full">
<td className="whitespace-nowrap px-6 py-5">
<div className="flex items-center gap-3">
<Image
src={invoice.image_url}
@@ -52,19 +110,19 @@ export default async function InvoicesTable({
<p>{invoice.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<td className="whitespace-nowrap px-3 py-4">
{invoice.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<td className="whitespace-nowrap px-3 py-4">
{formatCurrency(invoice.amount)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<td className="whitespace-nowrap px-3 py-4">
{formatDateToLocal(invoice.date)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<td className="whitespace-nowrap px-3 py-4">
<InvoiceStatus status={invoice.status} />
</td>
<td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4 text-sm">
<td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4">
<UpdateInvoice id={invoice.id} />
<DeleteInvoice id={invoice.id} />
</td>