mirror of
https://github.com/vercel/next-learn.git
synced 2026-07-01 16:44:30 +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() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<main>
|
||||||
<CustomersTable />
|
<CustomersTable />
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export default function Page({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<main>
|
||||||
<InvoicesTable searchParams={searchParams} />
|
<InvoicesTable searchParams={searchParams} />
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import TopNav from '@/app/ui/dashboard/topnav';
|
|
||||||
import SideNav from '@/app/ui/dashboard/sidenav';
|
import SideNav from '@/app/ui/dashboard/sidenav';
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
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">
|
<div className="w-14 md:w-64">
|
||||||
<SideNav />
|
<SideNav />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow overflow-y-auto">
|
<div className="flex-grow overflow-y-auto p-4 sm:p-10 md:p-20">
|
||||||
<TopNav />
|
{children}
|
||||||
<div className="p-4 sm:p-10 md:p-20">{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import './globals.css';
|
import './global.css';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -15,7 +16,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`bg-white ${inter.className}`}>{children}</body>
|
<body className={inter.className}>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ export type Invoice = {
|
|||||||
id: number;
|
id: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
status: 'pending' | 'paid'; // In TypeScript, this is called a string union type.
|
// In TypeScript, this is called a string union type.
|
||||||
// It means that the "status" property can only be one of the two strings.
|
// It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
|
||||||
|
status: 'pending' | 'paid';
|
||||||
date: string;
|
date: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,105 +43,105 @@ export const invoices: Invoice[] = [
|
|||||||
customerId: 1,
|
customerId: 1,
|
||||||
amount: 15795,
|
amount: 15795,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
date: '2023-12-01',
|
date: '2023-12-06',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
customerId: 2,
|
customerId: 2,
|
||||||
amount: 20348,
|
amount: 20348,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
date: '2023-11-01',
|
date: '2023-11-14',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 3040,
|
amount: 3040,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-10-01',
|
date: '2023-10-29',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
customerId: 4,
|
customerId: 4,
|
||||||
amount: 44800,
|
amount: 44800,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-09-01',
|
date: '2023-09-10',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
customerId: 1,
|
customerId: 1,
|
||||||
amount: 34577,
|
amount: 34577,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
date: '2023-08-01',
|
date: '2023-08-05',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
customerId: 2,
|
customerId: 2,
|
||||||
amount: 54246,
|
amount: 54246,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
date: '2023-07-01',
|
date: '2023-07-16',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 8945,
|
amount: 8945,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
date: '2023-06-01',
|
date: '2023-06-27',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
customerId: 4,
|
customerId: 4,
|
||||||
amount: 32545,
|
amount: 32545,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-06-01',
|
date: '2023-06-09',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 1250,
|
amount: 1250,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-06-02',
|
date: '2023-06-17',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
customerId: 1,
|
customerId: 1,
|
||||||
amount: 8945,
|
amount: 8945,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-06-01',
|
date: '2023-06-07',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
customerId: 2,
|
customerId: 2,
|
||||||
amount: 500,
|
amount: 500,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-08-01',
|
date: '2023-08-19',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 8945,
|
amount: 8945,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-06-01',
|
date: '2023-06-03',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 8945,
|
amount: 8945,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-06-01',
|
date: '2023-06-18',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
id: 14,
|
||||||
customerId: 4,
|
customerId: 4,
|
||||||
amount: 8945,
|
amount: 8945,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
date: '2023-10-01',
|
date: '2023-10-04',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 15,
|
||||||
customerId: 3,
|
customerId: 3,
|
||||||
amount: 1000,
|
amount: 1000,
|
||||||
status: 'paid',
|
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() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="flex flex-col gap-4 lg:h-screen lg:flex-row lg:items-center lg:justify-end">
|
||||||
<Hero />
|
<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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export default function BackgroundBlur() {
|
export default function BackgroundBlur() {
|
||||||
return (
|
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-[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-[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-[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">
|
<div className="flex w-full items-center justify-between">
|
||||||
<h1 className="text-base font-semibold">Customers</h1>
|
<h1 className="text-base font-semibold">Customers</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
<div className="mt-8 flow-root">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="overflow-hidden rounded-md border">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<thead className="bg-gray-50 text-left text-sm">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
<tr>
|
<thead className="bg-gray-50 text-left text-sm">
|
||||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
<tr>
|
||||||
<span className="sr-only">Profile</span>
|
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
|
||||||
</th>
|
Name
|
||||||
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
|
</th>
|
||||||
Name
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Email
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Email
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Total Invoices
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Total Invoices
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Total Pending
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Total Pending
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Total Paid
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
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>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
|
||||||
</table>
|
<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>
|
</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">
|
<p className="mt-2 truncate text-2xl font-semibold tracking-wide md:text-3xl">
|
||||||
{value}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1.5 text-sm text-gray-400">+00% since last month</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ export default function LatestInvoices({
|
|||||||
height={32}
|
height={32}
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0">
|
<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">
|
<p className="hidden text-sm text-gray-500 sm:block">
|
||||||
{customer?.email}
|
{customer?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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', {
|
{(invoice.amount / 100).toLocaleString('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD',
|
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 (
|
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>
|
<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">
|
<div className="sm:grid-cols-13 mt-4 grid grid-cols-12 items-end gap-2 md:gap-4">
|
||||||
{/* y-axis */}
|
{/* 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"
|
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
|
||||||
style={{ height: `${chartHeight}px` }}
|
style={{ height: `${chartHeight}px` }}
|
||||||
>
|
>
|
||||||
{yAxisLabels.map((label, index) => (
|
{yAxisLabels.map((label) => (
|
||||||
<p key={index}>{label}</p>
|
<p key={label}>{label}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{revenue.map((month, index) => (
|
{revenue.map((month) => (
|
||||||
<div key={index} className="flex flex-col items-center gap-2">
|
<div key={month.month} className="flex flex-col items-center gap-2">
|
||||||
{/* bars */}
|
{/* bars */}
|
||||||
<div
|
<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={{
|
style={{
|
||||||
height: `${(chartHeight / topLabel) * month.revenue}px`,
|
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 { PowerIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
import {
|
|
||||||
UserGroupIcon,
|
|
||||||
HomeIcon,
|
|
||||||
InboxIcon,
|
|
||||||
PowerIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { usePathname } from 'next/navigation';
|
import Link from 'next/link';
|
||||||
import clsx from 'clsx';
|
import NavLinks from '@/app/ui/dashboard/nav-links';
|
||||||
|
|
||||||
export default function SideNav() {
|
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 (
|
return (
|
||||||
<div className="flex h-full flex-col justify-between border-r px-2 py-4">
|
<div className="flex h-full flex-col justify-between border-r px-2 py-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -32,24 +16,7 @@ export default function SideNav() {
|
|||||||
height={32}
|
height={32}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
{tabs.map((tab, i) => {
|
<NavLinks />
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
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 (
|
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">
|
<h2 className="mb-6 text-xl font-semibold text-gray-900">
|
||||||
{type === 'new' ? 'New Invoice' : 'Edit Invoice'}
|
{type === 'new' ? 'New Invoice' : 'Edit Invoice'}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -84,7 +84,7 @@ export default function InvoiceForm({
|
|||||||
value={amount}
|
value={amount}
|
||||||
placeholder="00.00"
|
placeholder="00.00"
|
||||||
onChange={(e) => setAmount(Number(e.target.value))}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ export default function PaginationButtons({
|
|||||||
const NextPageTag = currentPage === totalPages ? 'p' : Link;
|
const NextPageTag = currentPage === totalPages ? 'p' : Link;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="inline-flex -space-x-px">
|
||||||
<PreviousPageTag
|
<PreviousPageTag
|
||||||
href={createPageUrl(currentPage - 1)}
|
href={createPageUrl(currentPage - 1)}
|
||||||
className={clsx(
|
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,
|
'text-gray-300': currentPage === 1,
|
||||||
},
|
},
|
||||||
@@ -45,9 +45,10 @@ export default function PaginationButtons({
|
|||||||
key={page}
|
key={page}
|
||||||
href={createPageUrl(page)}
|
href={createPageUrl(page)}
|
||||||
className={clsx(
|
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
|
<NextPageTag
|
||||||
href={createPageUrl(currentPage + 1)}
|
href={createPageUrl(currentPage + 1)}
|
||||||
className={clsx(
|
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,
|
'text-gray-300': currentPage === totalPages,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} 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 TableSearch from './table-search';
|
||||||
import PaginationButtons from './pagination';
|
import PaginationButtons from './pagination';
|
||||||
|
|
||||||
@@ -94,87 +94,91 @@ export default function InvoicesTable({
|
|||||||
Add Invoice
|
Add Invoice
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex items-center justify-between gap-2">
|
||||||
<TableSearch />
|
<TableSearch />
|
||||||
<PaginationButtons totalPages={totalPages} currentPage={currentPage} />
|
<PaginationButtons totalPages={totalPages} currentPage={currentPage} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4 flow-root">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="overflow-hidden rounded-md border">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<div className="overflow-hidden rounded-md border">
|
||||||
<thead className="bg-gray-50 text-left text-sm">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
<tr>
|
<thead className="bg-gray-50 text-left text-sm">
|
||||||
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
|
<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">
|
</th>
|
||||||
Customer
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Customer
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Email
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Email
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Amount
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Amount
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Date
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Date
|
||||||
<th scope="col" className="px-3 py-3.5 font-semibold">
|
</th>
|
||||||
Status
|
<th scope="col" className="px-3 py-3.5 font-semibold">
|
||||||
</th>
|
Status
|
||||||
|
</th>
|
||||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
<th
|
||||||
<span className="sr-only">Edit</span>
|
scope="col"
|
||||||
</th>
|
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
|
||||||
</tr>
|
>
|
||||||
</thead>
|
<span className="sr-only">Edit</span>
|
||||||
<tbody className="divide-y divide-gray-200 text-gray-500">
|
</th>
|
||||||
{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>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="divide-y divide-gray-200 text-gray-500">
|
||||||
</table>
|
{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>
|
</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