Polish Mobile Styles 💅🏼 and update code for chapters 1-5 (#150)

* Add local date formatting

* Add dashboard hero image

* Update hero styles

* Polish login form

* Use Next.js symbol for logo

* Use Next.js symbol for favicon

* Use img instead of Image

* Add mobile styles to login form

* Polish nav styles to fit logo

* Fix build error

* Create og-image.png

* Remove unused code

* Replace svg logo with png

* Update Images and Links

* Misc

* Remove topnav

* Remove dummy text from cards

* Remove gradient

* Adjust button color

* Fix table horizontal scroll

* Misc

* Fix table UI bug

* Remove duplicate package-lock files

* Polish invoice form

* Rename delete button

* Misc

* Run prettier

* Fix search and pagination on mobile

* Fix pagination border px bug

* Update code to match course

* Rename global -> globals

* 💅 Home Page

* Test placeholder blur

* Use 1.5x image rather than 2x

* Update root layout

* Use <main> for pages

* Make sidebar a server component

* Don't use index for React key
This commit is contained in:
Delba de Oliveira
2023-09-13 10:44:25 +01:00
committed by GitHub
parent 8e302d6725
commit 5f38f0fa81
23 changed files with 262 additions and 285 deletions

View File

@@ -2,8 +2,8 @@ import CustomersTable from '@/app/ui/customers/table';
export default function Page() {
return (
<div>
<main>
<CustomersTable />
</div>
</main>
);
}

View File

@@ -9,8 +9,8 @@ export default function Page({
};
}) {
return (
<div>
<main>
<InvoicesTable searchParams={searchParams} />
</div>
</main>
);
}

View File

@@ -1,4 +1,3 @@
import TopNav from '@/app/ui/dashboard/topnav';
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
@@ -7,9 +6,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<div className="w-14 md:w-64">
<SideNav />
</div>
<div className="flex-grow overflow-y-auto">
<TopNav />
<div className="p-4 sm:p-10 md:p-20">{children}</div>
<div className="flex-grow overflow-y-auto p-4 sm:p-10 md:p-20">
{children}
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
import './globals.css';
import './global.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
@@ -15,7 +16,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body className={`bg-white ${inter.className}`}>{children}</body>
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@@ -19,8 +19,9 @@ export type Invoice = {
id: number;
customerId: number;
amount: number;
status: 'pending' | 'paid'; // In TypeScript, this is called a string union type.
// It means that the "status" property can only be one of the two strings.
// In TypeScript, this is called a string union type.
// It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
status: 'pending' | 'paid';
date: string;
};

View File

@@ -43,105 +43,105 @@ export const invoices: Invoice[] = [
customerId: 1,
amount: 15795,
status: 'pending',
date: '2023-12-01',
date: '2023-12-06',
},
{
id: 2,
customerId: 2,
amount: 20348,
status: 'pending',
date: '2023-11-01',
date: '2023-11-14',
},
{
id: 3,
customerId: 3,
amount: 3040,
status: 'paid',
date: '2023-10-01',
date: '2023-10-29',
},
{
id: 4,
customerId: 4,
amount: 44800,
status: 'paid',
date: '2023-09-01',
date: '2023-09-10',
},
{
id: 5,
customerId: 1,
amount: 34577,
status: 'pending',
date: '2023-08-01',
date: '2023-08-05',
},
{
id: 6,
customerId: 2,
amount: 54246,
status: 'pending',
date: '2023-07-01',
date: '2023-07-16',
},
{
id: 7,
customerId: 3,
amount: 8945,
status: 'pending',
date: '2023-06-01',
date: '2023-06-27',
},
{
id: 8,
customerId: 4,
amount: 32545,
status: 'paid',
date: '2023-06-01',
date: '2023-06-09',
},
{
id: 9,
customerId: 3,
amount: 1250,
status: 'paid',
date: '2023-06-02',
date: '2023-06-17',
},
{
id: 10,
customerId: 1,
amount: 8945,
status: 'paid',
date: '2023-06-01',
date: '2023-06-07',
},
{
id: 11,
customerId: 2,
amount: 500,
status: 'paid',
date: '2023-08-01',
date: '2023-08-19',
},
{
id: 12,
customerId: 3,
amount: 8945,
status: 'paid',
date: '2023-06-01',
date: '2023-06-03',
},
{
id: 13,
customerId: 3,
amount: 8945,
status: 'paid',
date: '2023-06-01',
date: '2023-06-18',
},
{
id: 14,
customerId: 4,
amount: 8945,
status: 'paid',
date: '2023-10-01',
date: '2023-10-04',
},
{
id: 15,
customerId: 3,
amount: 1000,
status: 'paid',
date: '2022-06-12',
date: '2022-06-05',
},
];

View File

@@ -1,9 +1,28 @@
import Hero from '@/app/ui/hero';
import BackgroundBlur from '@/app/ui/background-blur';
import Image from 'next/image';
import HeroImage from '@/public/hero.png';
export default function Page() {
return (
<main>
<Hero />
<main className="flex flex-col gap-4 lg:h-screen lg:flex-row lg:items-center lg:justify-end">
<div className="min-w-xl my-8 flex flex-col items-start gap-4 px-4 lg:max-w-xl lg:gap-6">
<BackgroundBlur />
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
Next.js Dashboard
</h1>
<p className="leading-6 text-gray-900">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut vulputate
dapibus consectetur. Duis quis eros euismod.
</p>
<a href="/login">
<button className="rounded-md bg-black px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-gray-800">
Log in
</button>
</a>
</div>
<div className="w-full sm:w-1/2">
<Image src={HeroImage} alt="Dashboard Hero Image" placeholder="blur" />
</div>
</main>
);
}

View File

@@ -1,8 +1,8 @@
export default function BackgroundBlur() {
return (
<>
<div className="absolute left-[55%] top-[10%] -z-10 h-40 w-40 rounded-full bg-gradient-to-r from-blue-200 via-blue-300 to-blue-500 opacity-90 blur-3xl"></div>
<div className="absolute left-[50%] top-[18%] -z-10 h-40 w-40 transform rounded-full bg-gradient-to-r from-blue-200 via-blue-300 to-blue-500 opacity-60 blur-3xl"></div>
<div className="absolute left-[55%] top-[15%] -z-10 h-40 w-40 rounded-full bg-gradient-to-r from-blue-200 via-blue-300 to-blue-500 opacity-90 blur-3xl"></div>
<div className="absolute left-[50%] top-[25%] -z-10 h-40 w-40 transform rounded-full bg-gradient-to-r from-blue-200 via-blue-300 to-blue-500 opacity-60 blur-3xl"></div>
</>
);
}

View File

@@ -11,69 +11,71 @@ export default function CustomersTable() {
<div className="flex w-full items-center justify-between">
<h1 className="text-base font-semibold">Customers</h1>
</div>
<div className="mt-8">
<div className="mt-8 flow-root">
<div className="overflow-x-auto">
<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">
<tr>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Profile</span>
</th>
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
Name
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Email
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Invoices
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Pending
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Paid
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white text-gray-500">
{customers.map((customer) => (
<tr key={customer.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
<div className="flex w-7 flex-none items-center">
<Image
src={customer.imageUrl}
alt={customer.name}
className="rounded-full"
width={28}
height={28}
/>
</div>
</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
{customer.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{customer.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{countCustomerInvoices(invoices, customer.id)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(
invoices,
'pending',
customer.id,
)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(invoices, 'paid', customer.id)}
</td>
<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">
<tr>
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
Name
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Email
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Invoices
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Pending
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Paid
</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-gray-200 bg-white text-gray-500">
{customers.map((customer) => (
<tr key={customer.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
<div className="flex items-center gap-3">
<Image
src={customer.imageUrl}
className="rounded-full"
alt={customer.name}
width={28}
height={28}
/>
<p>{customer.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{customer.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{countCustomerInvoices(invoices, customer.id)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(
invoices,
'pending',
customer.id,
)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(
invoices,
'paid',
customer.id,
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@@ -34,7 +34,6 @@ export default function Card({
<p className="mt-2 truncate text-2xl font-semibold tracking-wide md:text-3xl">
{value}
</p>
<p className="mt-1.5 text-sm text-gray-400">+00% since last month</p>
</div>
);
}

View File

@@ -34,14 +34,15 @@ export default function LatestInvoices({
height={32}
/>
<div className="min-w-0">
<p className="truncate font-semibold">{customer?.name}</p>
<p className="truncate text-sm font-semibold md:text-base">
{customer?.name}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{customer?.email}
</p>
</div>
</div>
<p className="truncate font-medium sm:text-lg">
+{' '}
<p className="truncate text-sm font-medium md:text-base">
{(invoice.amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',

View File

@@ -0,0 +1,45 @@
'use client';
import {
UserGroupIcon,
HomeIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{ name: 'Invoices', href: '/dashboard/invoices', icon: InboxIcon },
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];
export default function NavLinks() {
const pathname = usePathname();
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className={clsx(
'mt-2 flex gap-2 rounded-md p-2 font-semibold hover:bg-gray-50 hover:text-blue-600',
{
'bg-gray-50 text-blue-600': pathname === link.href,
},
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}

View File

@@ -15,7 +15,7 @@ export default function RevenueChart({ revenue }: { revenue: Revenue[] }) {
}
return (
<div className="rounded-xl border p-6 shadow-sm shadow-sm md:col-span-5">
<div className="rounded-xl border p-6 shadow-sm md:col-span-5">
<h2 className="font-semibold">Revenue</h2>
<div className="sm:grid-cols-13 mt-4 grid grid-cols-12 items-end gap-2 md:gap-4">
{/* y-axis */}
@@ -23,16 +23,16 @@ export default function RevenueChart({ revenue }: { revenue: Revenue[] }) {
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
style={{ height: `${chartHeight}px` }}
>
{yAxisLabels.map((label, index) => (
<p key={index}>{label}</p>
{yAxisLabels.map((label) => (
<p key={label}>{label}</p>
))}
</div>
{revenue.map((month, index) => (
<div key={index} className="flex flex-col items-center gap-2">
{revenue.map((month) => (
<div key={month.month} className="flex flex-col items-center gap-2">
{/* bars */}
<div
className="w-full rounded-md bg-gradient-to-t from-blue-200 via-blue-300 to-blue-400"
className="w-full rounded-md bg-blue-300"
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}

View File

@@ -1,25 +0,0 @@
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
export default function Search() {
async function submitForm(formData: FormData) {
'use server';
// TODO: Implement search
}
return (
<div className="relative flex h-full w-full items-center px-4">
<MagnifyingGlassIcon className="h-5 text-gray-400" />
<form className="h-full w-full" action={submitForm}>
<label htmlFor="search-field" className="sr-only">
Search
</label>
<input
id="search-field"
className="h-full w-full border-0 px-2 text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-0"
placeholder="Search..."
type="search"
name="search"
/>
</form>
</div>
);
}

View File

@@ -1,25 +1,9 @@
'use client';
import {
UserGroupIcon,
HomeIcon,
InboxIcon,
PowerIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { PowerIcon } from '@heroicons/react/24/outline';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
export default function SideNav() {
const pathname = usePathname();
const tabs = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{ name: 'Invoices', href: '/dashboard/invoices', icon: InboxIcon },
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];
return (
<div className="flex h-full flex-col justify-between border-r px-2 py-4">
<div>
@@ -32,24 +16,7 @@ export default function SideNav() {
height={32}
/>
</Link>
{tabs.map((tab, i) => {
const TabIcon = tab.icon;
return (
<Link
key={i}
href={tab.href}
className={clsx(
'mt-2 flex gap-2 rounded-md p-2 font-semibold hover:bg-gray-50 hover:text-blue-600',
{
'bg-gray-50 text-blue-600': pathname === tab.href,
},
)}
>
<TabIcon className="w-6" />
<p className="hidden md:block">{tab.name}</p>
</Link>
);
})}
<NavLinks />
</div>
<Link
href="/login"

View File

@@ -1,9 +0,0 @@
import Search from '@/app/ui/dashboard/search';
export default function TopNav() {
return (
<div className="flex h-16 items-center border-b">
<Search />
</div>
);
}

View File

@@ -1,27 +0,0 @@
import BackgroundBlur from '@/app/ui/background-blur';
export default function Hero() {
return (
<div className="mx-auto max-w-4xl">
<div className="mx-auto mt-20 flex max-w-2xl flex-col items-center space-y-6 p-2">
<BackgroundBlur />
<h1 className="text-center text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
Next.js Dashboard
</h1>
<p className="text-center leading-6 text-gray-900">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut vulputate
dapibus consectetur. Duis quis eros euismod.
</p>
<a href="/login">
<button className="rounded-md bg-black px-4 py-2 text-sm font-semibold text-white hover:bg-gray-700">
Log in
</button>
</a>
</div>
<div className="mt-6 px-2">
<img src="/hero.png" alt="Dashboard image" />
</div>
</div>
);
}

View File

@@ -46,7 +46,7 @@ export default function InvoiceForm({
};
return (
<div className="mx-auto max-w-md p-4">
<div className="mx-auto max-w-sm rounded-lg border px-6 py-8 shadow-sm">
<h2 className="mb-6 text-xl font-semibold text-gray-900">
{type === 'new' ? 'New Invoice' : 'Edit Invoice'}
</h2>
@@ -84,7 +84,7 @@ export default function InvoiceForm({
value={amount}
placeholder="00.00"
onChange={(e) => setAmount(Number(e.target.value))}
className="block w-full rounded-md border-0 py-1.5 pl-7 text-sm leading-6 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400"
className="block w-full rounded-md border-0 py-1.5 pl-7 text-sm leading-6 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400"
/>
</div>
</div>

View File

@@ -26,11 +26,11 @@ export default function PaginationButtons({
const NextPageTag = currentPage === totalPages ? 'p' : Link;
return (
<div className="flex items-center justify-end">
<div className="inline-flex -space-x-px">
<PreviousPageTag
href={createPageUrl(currentPage - 1)}
className={clsx(
'flex h-8 w-8 items-center justify-center rounded-l-md border border-gray-300',
'flex h-9 w-9 items-center justify-center rounded-l-md ring-1 ring-inset ring-gray-300',
{
'text-gray-300': currentPage === 1,
},
@@ -45,9 +45,10 @@ export default function PaginationButtons({
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',
'flex h-9 w-9 items-center justify-center text-sm ring-1 ring-inset ring-gray-300',
{
'border-blue-600 bg-blue-600 text-white': currentPage === page,
'z-10 bg-blue-600 text-white ring-blue-600':
currentPage === page,
},
)}
>
@@ -58,7 +59,7 @@ export default function PaginationButtons({
<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',
'flex h-9 w-9 items-center justify-center rounded-r-md ring-1 ring-inset ring-gray-300',
{
'text-gray-300': currentPage === totalPages,
},

View File

@@ -7,7 +7,7 @@ import {
ClockIcon,
CheckCircleIcon,
} from '@heroicons/react/24/outline';
import DeleteInvoice from '@/app/ui/invoices/delete-invoice-button';
import DeleteInvoice from '@/app/ui/invoices/delete-button';
import TableSearch from './table-search';
import PaginationButtons from './pagination';
@@ -94,87 +94,91 @@ export default function InvoicesTable({
Add Invoice
</Link>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="mt-8 flex items-center justify-between gap-2">
<TableSearch />
<PaginationButtons totalPages={totalPages} currentPage={currentPage} />
</div>
<div className="mt-4">
<div className="mt-4 flow-root">
<div className="overflow-x-auto">
<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">
<tr>
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
#
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Customer
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Email
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Amount
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Date
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Status
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-500">
{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">
<Image
src={`${getCustomerById(invoice.customerId)
?.imageUrl}`}
className="rounded-full"
alt="Customer Image"
width={28}
height={28}
/>
<p>{getCustomerById(invoice.customerId)?.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{getCustomerById(invoice.customerId)?.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{(invoice.amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
})}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{formatDateToLocal(invoice.date)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{renderInvoiceStatus(invoice.status)}
</td>
<td className="flex justify-end gap-2 whitespace-nowrap py-4 pl-3 pr-6 text-sm">
<Link
href={`/dashboard/invoices/${invoice.id}/edit`}
className="rounded-md border p-1"
>
<PencilSquareIcon className="w-4" />
</Link>
<DeleteInvoice id={invoice.id} />
</td>
<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">
<tr>
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
#
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Customer
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Email
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Amount
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Date
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Status
</th>
<th
scope="col"
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<span className="sr-only">Edit</span>
</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-500">
{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">
<Image
src={`${getCustomerById(invoice.customerId)
?.imageUrl}`}
className="rounded-full"
alt="Customer Image"
width={28}
height={28}
/>
<p>{getCustomerById(invoice.customerId)?.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{getCustomerById(invoice.customerId)?.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{(invoice.amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
})}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{formatDateToLocal(invoice.date)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{renderInvoiceStatus(invoice.status)}
</td>
<td className="flex justify-end gap-2 whitespace-nowrap py-4 pl-3 pr-6 text-sm">
<Link
href={`/dashboard/invoices/${invoice.id}/edit`}
className="rounded-md border p-1"
>
<PencilSquareIcon className="w-4" />
</Link>
<DeleteInvoice id={invoice.id} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 696 KiB