First Commit
This commit is contained in:
@@ -1,13 +0,0 @@
|
|||||||
# Copy from .env.local on the Vercel dashboard
|
|
||||||
# https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
|
|
||||||
POSTGRES_URL=
|
|
||||||
POSTGRES_PRISMA_URL=
|
|
||||||
POSTGRES_URL_NON_POOLING=
|
|
||||||
POSTGRES_USER=
|
|
||||||
POSTGRES_HOST=
|
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
POSTGRES_DATABASE=
|
|
||||||
|
|
||||||
# `openssl rand -base64 32`
|
|
||||||
AUTH_SECRET=
|
|
||||||
AUTH_URL=http://localhost:3000/api/auth
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <p>Customers Page</p>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <p>Invoices Page</p>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import SideNav from '@/app/ui/dashboard/sidenav';
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
|
||||||
|
<div className="w-full flex-none md:w-64">
|
||||||
|
<SideNav />
|
||||||
|
</div>
|
||||||
|
<div className="grow p-6 md:overflow-y-auto md:p-12">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <p>Dashboard Page</p>;
|
||||||
|
}
|
||||||
+4
-1
@@ -1,3 +1,6 @@
|
|||||||
|
import '@/app/ui/global.css';
|
||||||
|
import { inter } from '@/app/ui/fonts';
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -5,7 +8,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>{children}</body>
|
<body className={`${inter.className} antialiased`}>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+38
-26
@@ -1,29 +1,31 @@
|
|||||||
import postgres from 'postgres';
|
import { sql } from '@vercel/postgres';
|
||||||
import {
|
import {
|
||||||
CustomerField,
|
CustomerField,
|
||||||
CustomersTableType,
|
CustomersTable,
|
||||||
InvoiceForm,
|
InvoiceForm,
|
||||||
InvoicesTable,
|
InvoicesTable,
|
||||||
LatestInvoiceRaw,
|
LatestInvoiceRaw,
|
||||||
|
User,
|
||||||
Revenue,
|
Revenue,
|
||||||
} from './definitions';
|
} from './definitions';
|
||||||
import { formatCurrency } from './utils';
|
import { formatCurrency } from './utils';
|
||||||
|
|
||||||
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
|
||||||
|
|
||||||
export async function fetchRevenue() {
|
export async function fetchRevenue() {
|
||||||
|
// Add noStore() here prevent the response from being cached.
|
||||||
|
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Artificially delay a response for demo purposes.
|
// Artificially delay a reponse for demo purposes.
|
||||||
// Don't do this in production :)
|
// Don't do this in real life :)
|
||||||
|
|
||||||
// console.log('Fetching revenue data...');
|
// console.log('Fetching revenue data...');
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 3000));
|
// await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
const data = await sql<Revenue[]>`SELECT * FROM revenue`;
|
const data = await sql<Revenue>`SELECT * FROM revenue`;
|
||||||
|
|
||||||
// console.log('Data fetch completed after 3 seconds.');
|
// console.log('Data fetch complete after 3 seconds.');
|
||||||
|
|
||||||
return data;
|
return data.rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database Error:', error);
|
console.error('Database Error:', error);
|
||||||
throw new Error('Failed to fetch revenue data.');
|
throw new Error('Failed to fetch revenue data.');
|
||||||
@@ -32,14 +34,14 @@ export async function fetchRevenue() {
|
|||||||
|
|
||||||
export async function fetchLatestInvoices() {
|
export async function fetchLatestInvoices() {
|
||||||
try {
|
try {
|
||||||
const data = await sql<LatestInvoiceRaw[]>`
|
const data = await sql<LatestInvoiceRaw>`
|
||||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
||||||
FROM invoices
|
FROM invoices
|
||||||
JOIN customers ON invoices.customer_id = customers.id
|
JOIN customers ON invoices.customer_id = customers.id
|
||||||
ORDER BY invoices.date DESC
|
ORDER BY invoices.date DESC
|
||||||
LIMIT 5`;
|
LIMIT 5`;
|
||||||
|
|
||||||
const latestInvoices = data.map((invoice) => ({
|
const latestInvoices = data.rows.map((invoice) => ({
|
||||||
...invoice,
|
...invoice,
|
||||||
amount: formatCurrency(invoice.amount),
|
amount: formatCurrency(invoice.amount),
|
||||||
}));
|
}));
|
||||||
@@ -68,10 +70,10 @@ export async function fetchCardData() {
|
|||||||
invoiceStatusPromise,
|
invoiceStatusPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const numberOfInvoices = Number(data[0][0].count ?? '0');
|
const numberOfInvoices = Number(data[0].rows[0].count ?? '0');
|
||||||
const numberOfCustomers = Number(data[1][0].count ?? '0');
|
const numberOfCustomers = Number(data[1].rows[0].count ?? '0');
|
||||||
const totalPaidInvoices = formatCurrency(data[2][0].paid ?? '0');
|
const totalPaidInvoices = formatCurrency(data[2].rows[0].paid ?? '0');
|
||||||
const totalPendingInvoices = formatCurrency(data[2][0].pending ?? '0');
|
const totalPendingInvoices = formatCurrency(data[2].rows[0].pending ?? '0');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
numberOfCustomers,
|
numberOfCustomers,
|
||||||
@@ -81,7 +83,7 @@ export async function fetchCardData() {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database Error:', error);
|
console.error('Database Error:', error);
|
||||||
throw new Error('Failed to fetch card data.');
|
throw new Error('Failed to card data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ export async function fetchFilteredInvoices(
|
|||||||
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
|
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const invoices = await sql<InvoicesTable[]>`
|
const invoices = await sql<InvoicesTable>`
|
||||||
SELECT
|
SELECT
|
||||||
invoices.id,
|
invoices.id,
|
||||||
invoices.amount,
|
invoices.amount,
|
||||||
@@ -114,7 +116,7 @@ export async function fetchFilteredInvoices(
|
|||||||
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
|
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return invoices;
|
return invoices.rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database Error:', error);
|
console.error('Database Error:', error);
|
||||||
throw new Error('Failed to fetch invoices.');
|
throw new Error('Failed to fetch invoices.');
|
||||||
@@ -123,7 +125,7 @@ export async function fetchFilteredInvoices(
|
|||||||
|
|
||||||
export async function fetchInvoicesPages(query: string) {
|
export async function fetchInvoicesPages(query: string) {
|
||||||
try {
|
try {
|
||||||
const data = await sql`SELECT COUNT(*)
|
const count = await sql`SELECT COUNT(*)
|
||||||
FROM invoices
|
FROM invoices
|
||||||
JOIN customers ON invoices.customer_id = customers.id
|
JOIN customers ON invoices.customer_id = customers.id
|
||||||
WHERE
|
WHERE
|
||||||
@@ -134,7 +136,7 @@ export async function fetchInvoicesPages(query: string) {
|
|||||||
invoices.status ILIKE ${`%${query}%`}
|
invoices.status ILIKE ${`%${query}%`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const totalPages = Math.ceil(Number(data[0].count) / ITEMS_PER_PAGE);
|
const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE);
|
||||||
return totalPages;
|
return totalPages;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database Error:', error);
|
console.error('Database Error:', error);
|
||||||
@@ -144,7 +146,7 @@ export async function fetchInvoicesPages(query: string) {
|
|||||||
|
|
||||||
export async function fetchInvoiceById(id: string) {
|
export async function fetchInvoiceById(id: string) {
|
||||||
try {
|
try {
|
||||||
const data = await sql<InvoiceForm[]>`
|
const data = await sql<InvoiceForm>`
|
||||||
SELECT
|
SELECT
|
||||||
invoices.id,
|
invoices.id,
|
||||||
invoices.customer_id,
|
invoices.customer_id,
|
||||||
@@ -154,7 +156,7 @@ export async function fetchInvoiceById(id: string) {
|
|||||||
WHERE invoices.id = ${id};
|
WHERE invoices.id = ${id};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const invoice = data.map((invoice) => ({
|
const invoice = data.rows.map((invoice) => ({
|
||||||
...invoice,
|
...invoice,
|
||||||
// Convert amount from cents to dollars
|
// Convert amount from cents to dollars
|
||||||
amount: invoice.amount / 100,
|
amount: invoice.amount / 100,
|
||||||
@@ -163,13 +165,12 @@ export async function fetchInvoiceById(id: string) {
|
|||||||
return invoice[0];
|
return invoice[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database Error:', error);
|
console.error('Database Error:', error);
|
||||||
throw new Error('Failed to fetch invoice.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCustomers() {
|
export async function fetchCustomers() {
|
||||||
try {
|
try {
|
||||||
const customers = await sql<CustomerField[]>`
|
const data = await sql<CustomerField>`
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name
|
name
|
||||||
@@ -177,6 +178,7 @@ export async function fetchCustomers() {
|
|||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const customers = data.rows;
|
||||||
return customers;
|
return customers;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Database Error:', err);
|
console.error('Database Error:', err);
|
||||||
@@ -186,7 +188,7 @@ export async function fetchCustomers() {
|
|||||||
|
|
||||||
export async function fetchFilteredCustomers(query: string) {
|
export async function fetchFilteredCustomers(query: string) {
|
||||||
try {
|
try {
|
||||||
const data = await sql<CustomersTableType[]>`
|
const data = await sql<CustomersTable>`
|
||||||
SELECT
|
SELECT
|
||||||
customers.id,
|
customers.id,
|
||||||
customers.name,
|
customers.name,
|
||||||
@@ -204,7 +206,7 @@ export async function fetchFilteredCustomers(query: string) {
|
|||||||
ORDER BY customers.name ASC
|
ORDER BY customers.name ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const customers = data.map((customer) => ({
|
const customers = data.rows.map((customer) => ({
|
||||||
...customer,
|
...customer,
|
||||||
total_pending: formatCurrency(customer.total_pending),
|
total_pending: formatCurrency(customer.total_pending),
|
||||||
total_paid: formatCurrency(customer.total_paid),
|
total_paid: formatCurrency(customer.total_paid),
|
||||||
@@ -216,3 +218,13 @@ export async function fetchFilteredCustomers(query: string) {
|
|||||||
throw new Error('Failed to fetch customer table.');
|
throw new Error('Failed to fetch customer table.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUser(email: string) {
|
||||||
|
try {
|
||||||
|
const user = await sql`SELECT * from USERS where email=${email}`;
|
||||||
|
return user.rows[0] as User;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user:', error);
|
||||||
|
throw new Error('Failed to fetch user.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export type InvoicesTable = {
|
|||||||
status: 'pending' | 'paid';
|
status: 'pending' | 'paid';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomersTableType = {
|
export type CustomersTable = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// This file contains placeholder data that you'll be replacing with real data in the Data Fetching chapter:
|
||||||
|
// https://nextjs.org/learn/dashboard-app/fetching-data
|
||||||
|
const users = [
|
||||||
|
{
|
||||||
|
id: '410544b2-4001-4271-9855-fec4b6a6442a',
|
||||||
|
name: 'User',
|
||||||
|
email: 'user@nextmail.com',
|
||||||
|
password: '123456',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const customers = [
|
||||||
|
{
|
||||||
|
id: '3958dc9e-712f-4377-85e9-fec4b6a6442a',
|
||||||
|
name: 'Delba de Oliveira',
|
||||||
|
email: 'delba@oliveira.com',
|
||||||
|
image_url: '/customers/delba-de-oliveira.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3958dc9e-742f-4377-85e9-fec4b6a6442a',
|
||||||
|
name: 'Lee Robinson',
|
||||||
|
email: 'lee@robinson.com',
|
||||||
|
image_url: '/customers/lee-robinson.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3958dc9e-737f-4377-85e9-fec4b6a6442a',
|
||||||
|
name: 'Hector Simpson',
|
||||||
|
email: 'hector@simpson.com',
|
||||||
|
image_url: '/customers/hector-simpson.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '50ca3e18-62cd-11ee-8c99-0242ac120002',
|
||||||
|
name: 'Steven Tey',
|
||||||
|
email: 'steven@tey.com',
|
||||||
|
image_url: '/customers/steven-tey.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3958dc9e-787f-4377-85e9-fec4b6a6442a',
|
||||||
|
name: 'Steph Dietz',
|
||||||
|
email: 'steph@dietz.com',
|
||||||
|
image_url: '/customers/steph-dietz.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '76d65c26-f784-44a2-ac19-586678f7c2f2',
|
||||||
|
name: 'Michael Novotny',
|
||||||
|
email: 'michael@novotny.com',
|
||||||
|
image_url: '/customers/michael-novotny.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa',
|
||||||
|
name: 'Evil Rabbit',
|
||||||
|
email: 'evil@rabbit.com',
|
||||||
|
image_url: '/customers/evil-rabbit.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '126eed9c-c90c-4ef6-a4a8-fcf7408d3c66',
|
||||||
|
name: 'Emil Kowalski',
|
||||||
|
email: 'emil@kowalski.com',
|
||||||
|
image_url: '/customers/emil-kowalski.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9',
|
||||||
|
name: 'Amy Burns',
|
||||||
|
email: 'amy@burns.com',
|
||||||
|
image_url: '/customers/amy-burns.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB',
|
||||||
|
name: 'Balazs Orban',
|
||||||
|
email: 'balazs@orban.com',
|
||||||
|
image_url: '/customers/balazs-orban.png',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const invoices = [
|
||||||
|
{
|
||||||
|
customer_id: customers[0].id,
|
||||||
|
amount: 15795,
|
||||||
|
status: 'pending',
|
||||||
|
date: '2022-12-06',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[1].id,
|
||||||
|
amount: 20348,
|
||||||
|
status: 'pending',
|
||||||
|
date: '2022-11-14',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[4].id,
|
||||||
|
amount: 3040,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2022-10-29',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[3].id,
|
||||||
|
amount: 44800,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-09-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[5].id,
|
||||||
|
amount: 34577,
|
||||||
|
status: 'pending',
|
||||||
|
date: '2023-08-05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[7].id,
|
||||||
|
amount: 54246,
|
||||||
|
status: 'pending',
|
||||||
|
date: '2023-07-16',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[6].id,
|
||||||
|
amount: 666,
|
||||||
|
status: 'pending',
|
||||||
|
date: '2023-06-27',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[3].id,
|
||||||
|
amount: 32545,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-06-09',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[4].id,
|
||||||
|
amount: 1250,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-06-17',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[5].id,
|
||||||
|
amount: 8546,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-06-07',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[1].id,
|
||||||
|
amount: 500,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-08-19',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[5].id,
|
||||||
|
amount: 8945,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-06-03',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[2].id,
|
||||||
|
amount: 8945,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-06-18',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[0].id,
|
||||||
|
amount: 8945,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2023-10-04',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customer_id: customers[2].id,
|
||||||
|
amount: 1000,
|
||||||
|
status: 'paid',
|
||||||
|
date: '2022-06-05',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const revenue = [
|
||||||
|
{ month: 'Jan', revenue: 2000 },
|
||||||
|
{ month: 'Feb', revenue: 1800 },
|
||||||
|
{ month: 'Mar', revenue: 2200 },
|
||||||
|
{ month: 'Apr', revenue: 2500 },
|
||||||
|
{ month: 'May', revenue: 2300 },
|
||||||
|
{ month: 'Jun', revenue: 3200 },
|
||||||
|
{ month: 'Jul', revenue: 3500 },
|
||||||
|
{ month: 'Aug', revenue: 3700 },
|
||||||
|
{ month: 'Sep', revenue: 2500 },
|
||||||
|
{ month: 'Oct', revenue: 2800 },
|
||||||
|
{ month: 'Nov', revenue: 3000 },
|
||||||
|
{ month: 'Dec', revenue: 4800 },
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
users,
|
||||||
|
customers,
|
||||||
|
invoices,
|
||||||
|
revenue,
|
||||||
|
};
|
||||||
+24
-6
@@ -1,16 +1,21 @@
|
|||||||
import AcmeLogo from '@/app/ui/acme-logo';
|
import AcmeLogo from '@/app/ui/acme-logo';
|
||||||
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
import styles from '@/app/ui/home.module.css';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { lusitana } from '@/app/ui/fonts';
|
||||||
|
import Image from 'next/image';
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col p-6">
|
<main className="flex min-h-screen flex-col p-6">
|
||||||
<div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
|
<div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
|
||||||
{/* <AcmeLogo /> */}
|
<AcmeLogo />
|
||||||
|
{/* <AcmeLogo /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
||||||
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
|
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
|
||||||
<p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}>
|
<div className={styles.shape} />
|
||||||
|
<p
|
||||||
|
className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}
|
||||||
|
>
|
||||||
<strong>Welcome to Acme.</strong> This is the example for the{' '}
|
<strong>Welcome to Acme.</strong> This is the example for the{' '}
|
||||||
<a href="https://nextjs.org/learn/" className="text-blue-500">
|
<a href="https://nextjs.org/learn/" className="text-blue-500">
|
||||||
Next.js Learn Course
|
Next.js Learn Course
|
||||||
@@ -21,11 +26,24 @@ export default function Page() {
|
|||||||
href="/login"
|
href="/login"
|
||||||
className="flex items-center gap-5 self-start rounded-lg bg-blue-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-400 md:text-base"
|
className="flex items-center gap-5 self-start rounded-lg bg-blue-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-400 md:text-base"
|
||||||
>
|
>
|
||||||
<span>Log in</span> <ArrowRightIcon className="w-5 md:w-6" />
|
<span>Log in</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
|
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
|
||||||
{/* Add Hero Images Here */}
|
<Image
|
||||||
|
src="/hero-desktop.png"
|
||||||
|
width={1000}
|
||||||
|
height={760}
|
||||||
|
className="hidden md:block"
|
||||||
|
alt="Screenshots of the dashboard project showing desktop version"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/hero-mobile.png"
|
||||||
|
width={560}
|
||||||
|
height={620}
|
||||||
|
className="block md:hidden"
|
||||||
|
alt="Screenshots of the dashboard project showing mobile version"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { lusitana } from '@/app/ui/fonts';
|
import { lusitana } from '@/app/ui/fonts';
|
||||||
import Search from '@/app/ui/search';
|
import Search from '../search';
|
||||||
import {
|
import { CustomersTable, FormattedCustomersTable } from '@/app/lib/definitions';
|
||||||
CustomersTableType,
|
|
||||||
FormattedCustomersTable,
|
|
||||||
} from '@/app/lib/definitions';
|
|
||||||
|
|
||||||
export default async function CustomersTable({
|
export default async function CustomersTable({
|
||||||
customers,
|
customers,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const iconMap = {
|
|||||||
invoices: InboxIcon,
|
invoices: InboxIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function CardWrapper() {
|
export default async function Cards() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* NOTE: Uncomment this code in Chapter 9 */}
|
{/* NOTE: comment in this code when you get to this point in the course */}
|
||||||
|
|
||||||
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" />
|
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" />
|
||||||
<Card title="Pending" value={totalPendingInvoices} type="pending" />
|
<Card title="Pending" value={totalPendingInvoices} type="pending" />
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export default async function LatestInvoices({
|
|||||||
latestInvoices: LatestInvoice[];
|
latestInvoices: LatestInvoice[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col md:col-span-4">
|
<div className="flex w-full flex-col md:col-span-4 lg:col-span-4">
|
||||||
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
|
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
|
||||||
Latest Invoices
|
Latest Invoices
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
|
<div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
|
||||||
{/* NOTE: Uncomment this code in Chapter 7 */}
|
{/* NOTE: comment in this code when you get to this point in the course */}
|
||||||
|
|
||||||
{/* <div className="bg-white px-6">
|
{/* <div className="bg-white px-6">
|
||||||
{latestInvoices.map((invoice, i) => {
|
{latestInvoices.map((invoice, i) => {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserGroupIcon,
|
UserGroupIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
DocumentDuplicateIcon,
|
DocumentDuplicateIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} 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.
|
// Map of links to display in the side navigation.
|
||||||
// Depending on the size of the application, this would be stored in a database.
|
// Depending on the size of the application, this would be stored in a database.
|
||||||
@@ -17,19 +22,25 @@ const links = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function NavLinks() {
|
export default function NavLinks() {
|
||||||
return (
|
const pathname = usePathname();
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{links.map((link) => {
|
{links.map((link) => {
|
||||||
const LinkIcon = link.icon;
|
const LinkIcon = link.icon;
|
||||||
return (
|
return (
|
||||||
<a
|
<Link
|
||||||
key={link.name}
|
key={link.name}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
|
className={clsx(
|
||||||
|
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
|
||||||
|
{
|
||||||
|
'bg-sky-100 text-blue-600': pathname === link.href,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<LinkIcon className="w-6" />
|
<LinkIcon className="w-6" />
|
||||||
<p className="hidden md:block">{link.name}</p>
|
<p className="hidden md:block">{link.name}</p>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default async function RevenueChart({
|
|||||||
revenue: Revenue[];
|
revenue: Revenue[];
|
||||||
}) {
|
}) {
|
||||||
const chartHeight = 350;
|
const chartHeight = 350;
|
||||||
// NOTE: Uncomment this code in Chapter 7
|
// NOTE: comment in this code when you get to this point in the course
|
||||||
|
|
||||||
// const { yAxisLabels, topLabel } = generateYAxis(revenue);
|
// const { yAxisLabels, topLabel } = generateYAxis(revenue);
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export default async function RevenueChart({
|
|||||||
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
|
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
|
||||||
Recent Revenue
|
Recent Revenue
|
||||||
</h2>
|
</h2>
|
||||||
{/* NOTE: Uncomment this code in Chapter 7 */}
|
{/* NOTE: comment in this code when you get to this point in the course */}
|
||||||
|
|
||||||
{/* <div className="rounded-xl bg-gray-50 p-4">
|
{/* <div className="rounded-xl bg-gray-50 p-4">
|
||||||
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
|
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function SideNav() {
|
|||||||
<NavLinks />
|
<NavLinks />
|
||||||
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
|
||||||
<form>
|
<form>
|
||||||
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
|
<button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
|
||||||
<PowerIcon className="w-6" />
|
<PowerIcon className="w-6" />
|
||||||
<div className="hidden md:block">Sign Out</div>
|
<div className="hidden md:block">Sign Out</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Inter, Lusitana } from 'next/font/google';
|
||||||
|
|
||||||
|
export const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
|
export const lusitana = Lusitana({
|
||||||
|
weight: ['400', '700'],
|
||||||
|
subsets: ['latin'],
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.shape {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
border-bottom: 30px solid black;
|
||||||
|
border-left: 20px solid transparent;
|
||||||
|
border-right: 20px solid transparent;
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ export function UpdateInvoice({ id }: { id: string }) {
|
|||||||
export function DeleteInvoice({ id }: { id: string }) {
|
export function DeleteInvoice({ id }: { id: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
|
<button className="rounded-md border p-2 hover:bg-gray-100">
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
<TrashIcon className="w-5" />
|
<TrashIcon className="w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { CustomerField } from '@/app/lib/definitions';
|
import { CustomerField } from '@/app/lib/definitions';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +8,7 @@ import {
|
|||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
UserCircleIcon,
|
UserCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { Button } from '@/app/ui/button';
|
import { Button } from '../button';
|
||||||
|
|
||||||
export default function Form({ customers }: { customers: CustomerField[] }) {
|
export default function Form({ customers }: { customers: CustomerField[] }) {
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +23,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
<select
|
<select
|
||||||
id="customer"
|
id="customer"
|
||||||
name="customerId"
|
name="customerId"
|
||||||
className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
@@ -55,13 +57,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
s
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invoice Status */}
|
{/* Invoice Status */}
|
||||||
<fieldset>
|
<div>
|
||||||
<legend className="mb-2 block text-sm font-medium">
|
<label htmlFor="status" className="mb-2 block text-sm font-medium">
|
||||||
Set the invoice status
|
Set the invoice status
|
||||||
</legend>
|
</label>
|
||||||
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -70,11 +73,11 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
name="status"
|
name="status"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="pending"
|
value="pending"
|
||||||
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
|
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="pending"
|
htmlFor="pending"
|
||||||
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600"
|
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Pending <ClockIcon className="h-4 w-4" />
|
Pending <ClockIcon className="h-4 w-4" />
|
||||||
</label>
|
</label>
|
||||||
@@ -85,18 +88,18 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
name="status"
|
name="status"
|
||||||
type="radio"
|
type="radio"
|
||||||
value="paid"
|
value="paid"
|
||||||
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
|
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="paid"
|
htmlFor="paid"
|
||||||
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white"
|
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Paid <CheckIcon className="h-4 w-4" />
|
Paid <CheckIcon className="h-4 w-4" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex justify-end gap-4">
|
<div className="mt-6 flex justify-end gap-4">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export default function EditInvoiceForm({
|
|||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
<div className="rounded-md bg-gray-50 p-4 md:p-6">
|
<div className="rounded-md bg-gray-50 p-4 md:p-6">
|
||||||
|
{/* Invoice ID */}
|
||||||
|
<input type="hidden" name="id" value={invoice.id} />
|
||||||
{/* Customer Name */}
|
{/* Customer Name */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
|
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
|
||||||
@@ -29,7 +31,7 @@ export default function EditInvoiceForm({
|
|||||||
<select
|
<select
|
||||||
id="customer"
|
id="customer"
|
||||||
name="customerId"
|
name="customerId"
|
||||||
className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||||
defaultValue={invoice.customer_id}
|
defaultValue={invoice.customer_id}
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
@@ -56,7 +58,6 @@ export default function EditInvoiceForm({
|
|||||||
id="amount"
|
id="amount"
|
||||||
name="amount"
|
name="amount"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
|
||||||
defaultValue={invoice.amount}
|
defaultValue={invoice.amount}
|
||||||
placeholder="Enter USD amount"
|
placeholder="Enter USD amount"
|
||||||
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||||
@@ -67,10 +68,10 @@ export default function EditInvoiceForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invoice Status */}
|
{/* Invoice Status */}
|
||||||
<fieldset>
|
<div>
|
||||||
<legend className="mb-2 block text-sm font-medium">
|
<label htmlFor="status" className="mb-2 block text-sm font-medium">
|
||||||
Set the invoice status
|
Set the invoice status
|
||||||
</legend>
|
</label>
|
||||||
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -80,11 +81,11 @@ export default function EditInvoiceForm({
|
|||||||
type="radio"
|
type="radio"
|
||||||
value="pending"
|
value="pending"
|
||||||
defaultChecked={invoice.status === 'pending'}
|
defaultChecked={invoice.status === 'pending'}
|
||||||
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
|
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="pending"
|
htmlFor="pending"
|
||||||
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600"
|
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Pending <ClockIcon className="h-4 w-4" />
|
Pending <ClockIcon className="h-4 w-4" />
|
||||||
</label>
|
</label>
|
||||||
@@ -96,18 +97,18 @@ export default function EditInvoiceForm({
|
|||||||
type="radio"
|
type="radio"
|
||||||
value="paid"
|
value="paid"
|
||||||
defaultChecked={invoice.status === 'paid'}
|
defaultChecked={invoice.status === 'paid'}
|
||||||
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
|
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="paid"
|
htmlFor="paid"
|
||||||
className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white"
|
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Paid <CheckIcon className="h-4 w-4" />
|
Paid <CheckIcon className="h-4 w-4" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex justify-end gap-4">
|
<div className="mt-6 flex justify-end gap-4">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import Link from 'next/link';
|
|||||||
import { generatePagination } from '@/app/lib/utils';
|
import { generatePagination } from '@/app/lib/utils';
|
||||||
|
|
||||||
export default function Pagination({ totalPages }: { totalPages: number }) {
|
export default function Pagination({ totalPages }: { totalPages: number }) {
|
||||||
// NOTE: Uncomment this code in Chapter 10
|
// NOTE: comment in this code when you get to this point in the course
|
||||||
|
|
||||||
// const allPages = generatePagination(currentPage, totalPages);
|
// const allPages = generatePagination(currentPage, totalPages);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* NOTE: Uncomment this code in Chapter 10 */}
|
{/* NOTE: comment in this code when you get to this point in the course */}
|
||||||
|
|
||||||
{/* <div className="inline-flex">
|
{/* <div className="inline-flex">
|
||||||
<PaginationArrow
|
<PaginationArrow
|
||||||
@@ -32,7 +32,7 @@ export default function Pagination({ totalPages }: { totalPages: number }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginationNumber
|
<PaginationNumber
|
||||||
key={`${page}-${index}`}
|
key={page}
|
||||||
href={createPageURL(page)}
|
href={createPageURL(page)}
|
||||||
page={page}
|
page={page}
|
||||||
position={position}
|
position={position}
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ export default function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-4 w-full">
|
<LoginButton />
|
||||||
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex h-8 items-end space-x-1">
|
<div className="flex h-8 items-end space-x-1">
|
||||||
{/* Add form errors here */}
|
{/* Add form errors here */}
|
||||||
</div>
|
</div>
|
||||||
@@ -65,3 +63,11 @@ export default function LoginForm() {
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LoginButton() {
|
||||||
|
return (
|
||||||
|
<Button className="mt-4 w-full">
|
||||||
|
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function RevenueChartSkeleton() {
|
|||||||
<div className={`${shimmer} relative w-full overflow-hidden md:col-span-4`}>
|
<div className={`${shimmer} relative w-full overflow-hidden md:col-span-4`}>
|
||||||
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
|
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
|
||||||
<div className="rounded-xl bg-gray-100 p-4">
|
<div className="rounded-xl bg-gray-100 p-4">
|
||||||
<div className="sm:grid-cols-13 mt-0 grid h-[410px] grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4" />
|
<div className="sm:grid-cols-13 mt-0 grid h-[410px] grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4" />
|
||||||
<div className="flex items-center pb-2 pt-6">
|
<div className="flex items-center pb-2 pt-6">
|
||||||
<div className="h-5 w-5 rounded-full bg-gray-200" />
|
<div className="h-5 w-5 rounded-full bg-gray-200" />
|
||||||
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
|
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
|
||||||
@@ -62,7 +62,7 @@ export function InvoiceSkeleton() {
|
|||||||
export function LatestInvoicesSkeleton() {
|
export function LatestInvoicesSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${shimmer} relative flex w-full flex-col overflow-hidden md:col-span-4`}
|
className={`${shimmer} relative flex w-full flex-col overflow-hidden md:col-span-4 lg:col-span-4`}
|
||||||
>
|
>
|
||||||
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
|
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
|
||||||
<div className="flex grow flex-col justify-between rounded-xl bg-gray-100 p-4">
|
<div className="flex grow flex-col justify-between rounded-xl bg-gray-100 p-4">
|
||||||
@@ -72,10 +72,10 @@ export function LatestInvoicesSkeleton() {
|
|||||||
<InvoiceSkeleton />
|
<InvoiceSkeleton />
|
||||||
<InvoiceSkeleton />
|
<InvoiceSkeleton />
|
||||||
<InvoiceSkeleton />
|
<InvoiceSkeleton />
|
||||||
</div>
|
<div className="flex items-center pb-2 pt-6">
|
||||||
<div className="flex items-center pb-2 pt-6">
|
<div className="h-5 w-5 rounded-full bg-gray-200" />
|
||||||
<div className="h-5 w-5 rounded-full bg-gray-200" />
|
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
|
||||||
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user