-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
53 changed files
with
4,433 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
.env | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## Next.js App Router Course - Starter | ||
|
||
This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application. | ||
|
||
For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <p>Customers Page</p>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <p>Invoices Page</p>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Page() { | ||
return <p>Dashboard Page</p>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import "@/app/ui/global.css"; | ||
import { inter } from "./ui/fonts"; | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
return ( | ||
<html lang="en"> | ||
<body className={`${inter.className} antialiased`}>{children}</body> | ||
</html> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { sql } from '@vercel/postgres'; | ||
import { | ||
CustomerField, | ||
CustomersTableType, | ||
InvoiceForm, | ||
InvoicesTable, | ||
LatestInvoiceRaw, | ||
Revenue, | ||
} from './definitions'; | ||
import { formatCurrency } from './utils'; | ||
|
||
export async function fetchRevenue() { | ||
try { | ||
// Artificially delay a response for demo purposes. | ||
// Don't do this in production :) | ||
|
||
// console.log('Fetching revenue data...'); | ||
// await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
|
||
const data = await sql<Revenue>`SELECT * FROM revenue`; | ||
|
||
// console.log('Data fetch completed after 3 seconds.'); | ||
|
||
return data.rows; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch revenue data.'); | ||
} | ||
} | ||
|
||
export async function fetchLatestInvoices() { | ||
try { | ||
const data = await sql<LatestInvoiceRaw>` | ||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id | ||
FROM invoices | ||
JOIN customers ON invoices.customer_id = customers.id | ||
ORDER BY invoices.date DESC | ||
LIMIT 5`; | ||
|
||
const latestInvoices = data.rows.map((invoice) => ({ | ||
...invoice, | ||
amount: formatCurrency(invoice.amount), | ||
})); | ||
return latestInvoices; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch the latest invoices.'); | ||
} | ||
} | ||
|
||
export async function fetchCardData() { | ||
try { | ||
// You can probably combine these into a single SQL query | ||
// However, we are intentionally splitting them to demonstrate | ||
// how to initialize multiple queries in parallel with JS. | ||
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`; | ||
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`; | ||
const invoiceStatusPromise = sql`SELECT | ||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid", | ||
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending" | ||
FROM invoices`; | ||
|
||
const data = await Promise.all([ | ||
invoiceCountPromise, | ||
customerCountPromise, | ||
invoiceStatusPromise, | ||
]); | ||
|
||
const numberOfInvoices = Number(data[0].rows[0].count ?? '0'); | ||
const numberOfCustomers = Number(data[1].rows[0].count ?? '0'); | ||
const totalPaidInvoices = formatCurrency(data[2].rows[0].paid ?? '0'); | ||
const totalPendingInvoices = formatCurrency(data[2].rows[0].pending ?? '0'); | ||
|
||
return { | ||
numberOfCustomers, | ||
numberOfInvoices, | ||
totalPaidInvoices, | ||
totalPendingInvoices, | ||
}; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch card data.'); | ||
} | ||
} | ||
|
||
const ITEMS_PER_PAGE = 6; | ||
export async function fetchFilteredInvoices( | ||
query: string, | ||
currentPage: number, | ||
) { | ||
const offset = (currentPage - 1) * ITEMS_PER_PAGE; | ||
|
||
try { | ||
const invoices = await sql<InvoicesTable>` | ||
SELECT | ||
invoices.id, | ||
invoices.amount, | ||
invoices.date, | ||
invoices.status, | ||
customers.name, | ||
customers.email, | ||
customers.image_url | ||
FROM invoices | ||
JOIN customers ON invoices.customer_id = customers.id | ||
WHERE | ||
customers.name ILIKE ${`%${query}%`} OR | ||
customers.email ILIKE ${`%${query}%`} OR | ||
invoices.amount::text ILIKE ${`%${query}%`} OR | ||
invoices.date::text ILIKE ${`%${query}%`} OR | ||
invoices.status ILIKE ${`%${query}%`} | ||
ORDER BY invoices.date DESC | ||
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset} | ||
`; | ||
|
||
return invoices.rows; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch invoices.'); | ||
} | ||
} | ||
|
||
export async function fetchInvoicesPages(query: string) { | ||
try { | ||
const count = await sql`SELECT COUNT(*) | ||
FROM invoices | ||
JOIN customers ON invoices.customer_id = customers.id | ||
WHERE | ||
customers.name ILIKE ${`%${query}%`} OR | ||
customers.email ILIKE ${`%${query}%`} OR | ||
invoices.amount::text ILIKE ${`%${query}%`} OR | ||
invoices.date::text ILIKE ${`%${query}%`} OR | ||
invoices.status ILIKE ${`%${query}%`} | ||
`; | ||
|
||
const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE); | ||
return totalPages; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch total number of invoices.'); | ||
} | ||
} | ||
|
||
export async function fetchInvoiceById(id: string) { | ||
try { | ||
const data = await sql<InvoiceForm>` | ||
SELECT | ||
invoices.id, | ||
invoices.customer_id, | ||
invoices.amount, | ||
invoices.status | ||
FROM invoices | ||
WHERE invoices.id = ${id}; | ||
`; | ||
|
||
const invoice = data.rows.map((invoice) => ({ | ||
...invoice, | ||
// Convert amount from cents to dollars | ||
amount: invoice.amount / 100, | ||
})); | ||
|
||
return invoice[0]; | ||
} catch (error) { | ||
console.error('Database Error:', error); | ||
throw new Error('Failed to fetch invoice.'); | ||
} | ||
} | ||
|
||
export async function fetchCustomers() { | ||
try { | ||
const data = await sql<CustomerField>` | ||
SELECT | ||
id, | ||
name | ||
FROM customers | ||
ORDER BY name ASC | ||
`; | ||
|
||
const customers = data.rows; | ||
return customers; | ||
} catch (err) { | ||
console.error('Database Error:', err); | ||
throw new Error('Failed to fetch all customers.'); | ||
} | ||
} | ||
|
||
export async function fetchFilteredCustomers(query: string) { | ||
try { | ||
const data = await sql<CustomersTableType>` | ||
SELECT | ||
customers.id, | ||
customers.name, | ||
customers.email, | ||
customers.image_url, | ||
COUNT(invoices.id) AS total_invoices, | ||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending, | ||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid | ||
FROM customers | ||
LEFT JOIN invoices ON customers.id = invoices.customer_id | ||
WHERE | ||
customers.name ILIKE ${`%${query}%`} OR | ||
customers.email ILIKE ${`%${query}%`} | ||
GROUP BY customers.id, customers.name, customers.email, customers.image_url | ||
ORDER BY customers.name ASC | ||
`; | ||
|
||
const customers = data.rows.map((customer) => ({ | ||
...customer, | ||
total_pending: formatCurrency(customer.total_pending), | ||
total_paid: formatCurrency(customer.total_paid), | ||
})); | ||
|
||
return customers; | ||
} catch (err) { | ||
console.error('Database Error:', err); | ||
throw new Error('Failed to fetch customer table.'); | ||
} | ||
} |
Oops, something went wrong.