-
Notifications
You must be signed in to change notification settings - Fork 0
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
0 parents
commit 7283820
Showing
53 changed files
with
7,165 additions
and
0 deletions.
There are no files selected for viewing
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,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} |
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 @@ | ||
18 |
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,11 @@ | ||
export default function RootLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
return ( | ||
<html lang="en"> | ||
<body>{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,230 @@ | ||
import { sql } from '@vercel/postgres'; | ||
import { | ||
CustomerField, | ||
CustomersTable, | ||
InvoiceForm, | ||
InvoicesTable, | ||
LatestInvoiceRaw, | ||
User, | ||
Revenue, | ||
} from './definitions'; | ||
import { formatCurrency } from './utils'; | ||
|
||
export async function fetchRevenue() { | ||
// Add noStore() here prevent the response from being cached. | ||
// This is equivalent to in fetch(..., {cache: 'no-store'}). | ||
|
||
try { | ||
// Artificially delay a reponse for demo purposes. | ||
// Don't do this in real life :) | ||
|
||
// console.log('Fetching revenue data...'); | ||
// await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
|
||
const data = await sql<Revenue>`SELECT * FROM revenue`; | ||
|
||
// console.log('Data fetch complete 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 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); | ||
} | ||
} | ||
|
||
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<CustomersTable>` | ||
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.'); | ||
} | ||
} | ||
|
||
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.'); | ||
} | ||
} |
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,88 @@ | ||
// This file contains type definitions for your data. | ||
// It describes the shape of the data, and what data type each property should accept. | ||
// For simplicity of teaching, we're manually defining these types. | ||
// However, these types are generated automatically if you're using an ORM such as Prisma. | ||
export type User = { | ||
id: string; | ||
name: string; | ||
email: string; | ||
password: string; | ||
}; | ||
|
||
export type Customer = { | ||
id: string; | ||
name: string; | ||
email: string; | ||
image_url: string; | ||
}; | ||
|
||
export type Invoice = { | ||
id: string; | ||
customer_id: string; | ||
amount: number; | ||
date: string; | ||
// 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'; | ||
}; | ||
|
||
export type Revenue = { | ||
month: string; | ||
revenue: number; | ||
}; | ||
|
||
export type LatestInvoice = { | ||
id: string; | ||
name: string; | ||
image_url: string; | ||
email: string; | ||
amount: string; | ||
}; | ||
|
||
// The database returns a number for amount, but we later format it to a string with the formatCurrency function | ||
export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & { | ||
amount: number; | ||
}; | ||
|
||
export type InvoicesTable = { | ||
id: string; | ||
customer_id: string; | ||
name: string; | ||
email: string; | ||
image_url: string; | ||
date: string; | ||
amount: number; | ||
status: 'pending' | 'paid'; | ||
}; | ||
|
||
export type CustomersTable = { | ||
id: string; | ||
name: string; | ||
email: string; | ||
image_url: string; | ||
total_invoices: number; | ||
total_pending: number; | ||
total_paid: number; | ||
}; | ||
|
||
export type FormattedCustomersTable = { | ||
id: string; | ||
name: string; | ||
email: string; | ||
image_url: string; | ||
total_invoices: number; | ||
total_pending: string; | ||
total_paid: string; | ||
}; | ||
|
||
export type CustomerField = { | ||
id: string; | ||
name: string; | ||
}; | ||
|
||
export type InvoiceForm = { | ||
id: string; | ||
customer_id: string; | ||
amount: number; | ||
status: 'pending' | 'paid'; | ||
}; |
Oops, something went wrong.