mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-28 15:14:15 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function fetchFilteredInvoices(
|
||||
query: string,
|
||||
currentPage: number,
|
||||
) {
|
||||
const itemsPerPage = 10;
|
||||
const itemsPerPage = 6;
|
||||
const offset = (currentPage - 1) * itemsPerPage;
|
||||
|
||||
try {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user