mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-25 13:46:10 +00:00
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:
committed by
GitHub
parent
8e302d6725
commit
5f38f0fa81
@@ -2,8 +2,8 @@ import CustomersTable from '@/app/ui/customers/table';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<CustomersTable />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ export default function Page({
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<InvoicesTable searchParams={searchParams} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
45
dashboard/15-final/app/ui/dashboard/nav-links.tsx
Normal file
45
dashboard/15-final/app/ui/dashboard/nav-links.tsx
Normal 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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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`,
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user