Skip to content

Commit ed08dd5

Browse files
committed
Initial commit from Create Next App
0 parents  commit ed08dd5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+4189
-0
lines changed

.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copy from .env.local on the Vercel dashboard
2+
# https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
3+
POSTGRES_URL=
4+
POSTGRES_PRISMA_URL=
5+
POSTGRES_URL_NON_POOLING=
6+
POSTGRES_USER=
7+
POSTGRES_HOST=
8+
POSTGRES_PASSWORD=
9+
POSTGRES_DATABASE=
10+
11+
# `openssl rand -base64 32`
12+
AUTH_SECRET=
13+
AUTH_URL=http://localhost:3000/api/auth

.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
.env
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Next.js App Router Course - Starter
2+
3+
This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application.
4+
5+
For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.

app/layout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function RootLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode;
5+
}) {
6+
return (
7+
<html lang="en">
8+
<body>{children}</body>
9+
</html>
10+
);
11+
}

app/lib/data.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import postgres from 'postgres';
2+
import {
3+
CustomerField,
4+
CustomersTableType,
5+
InvoiceForm,
6+
InvoicesTable,
7+
LatestInvoiceRaw,
8+
Revenue,
9+
} from './definitions';
10+
import { formatCurrency } from './utils';
11+
12+
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
13+
14+
export async function fetchRevenue() {
15+
try {
16+
// Artificially delay a response for demo purposes.
17+
// Don't do this in production :)
18+
19+
// console.log('Fetching revenue data...');
20+
// await new Promise((resolve) => setTimeout(resolve, 3000));
21+
22+
const data = await sql<Revenue[]>`SELECT * FROM revenue`;
23+
24+
// console.log('Data fetch completed after 3 seconds.');
25+
26+
return data;
27+
} catch (error) {
28+
console.error('Database Error:', error);
29+
throw new Error('Failed to fetch revenue data.');
30+
}
31+
}
32+
33+
export async function fetchLatestInvoices() {
34+
try {
35+
const data = await sql<LatestInvoiceRaw[]>`
36+
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
37+
FROM invoices
38+
JOIN customers ON invoices.customer_id = customers.id
39+
ORDER BY invoices.date DESC
40+
LIMIT 5`;
41+
42+
const latestInvoices = data.map((invoice) => ({
43+
...invoice,
44+
amount: formatCurrency(invoice.amount),
45+
}));
46+
return latestInvoices;
47+
} catch (error) {
48+
console.error('Database Error:', error);
49+
throw new Error('Failed to fetch the latest invoices.');
50+
}
51+
}
52+
53+
export async function fetchCardData() {
54+
try {
55+
// You can probably combine these into a single SQL query
56+
// However, we are intentionally splitting them to demonstrate
57+
// how to initialize multiple queries in parallel with JS.
58+
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
59+
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
60+
const invoiceStatusPromise = sql`SELECT
61+
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
62+
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
63+
FROM invoices`;
64+
65+
const data = await Promise.all([
66+
invoiceCountPromise,
67+
customerCountPromise,
68+
invoiceStatusPromise,
69+
]);
70+
71+
const numberOfInvoices = Number(data[0][0].count ?? '0');
72+
const numberOfCustomers = Number(data[1][0].count ?? '0');
73+
const totalPaidInvoices = formatCurrency(data[2][0].paid ?? '0');
74+
const totalPendingInvoices = formatCurrency(data[2][0].pending ?? '0');
75+
76+
return {
77+
numberOfCustomers,
78+
numberOfInvoices,
79+
totalPaidInvoices,
80+
totalPendingInvoices,
81+
};
82+
} catch (error) {
83+
console.error('Database Error:', error);
84+
throw new Error('Failed to fetch card data.');
85+
}
86+
}
87+
88+
const ITEMS_PER_PAGE = 6;
89+
export async function fetchFilteredInvoices(
90+
query: string,
91+
currentPage: number,
92+
) {
93+
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
94+
95+
try {
96+
const invoices = await sql<InvoicesTable[]>`
97+
SELECT
98+
invoices.id,
99+
invoices.amount,
100+
invoices.date,
101+
invoices.status,
102+
customers.name,
103+
customers.email,
104+
customers.image_url
105+
FROM invoices
106+
JOIN customers ON invoices.customer_id = customers.id
107+
WHERE
108+
customers.name ILIKE ${`%${query}%`} OR
109+
customers.email ILIKE ${`%${query}%`} OR
110+
invoices.amount::text ILIKE ${`%${query}%`} OR
111+
invoices.date::text ILIKE ${`%${query}%`} OR
112+
invoices.status ILIKE ${`%${query}%`}
113+
ORDER BY invoices.date DESC
114+
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
115+
`;
116+
117+
return invoices;
118+
} catch (error) {
119+
console.error('Database Error:', error);
120+
throw new Error('Failed to fetch invoices.');
121+
}
122+
}
123+
124+
export async function fetchInvoicesPages(query: string) {
125+
try {
126+
const data = await sql`SELECT COUNT(*)
127+
FROM invoices
128+
JOIN customers ON invoices.customer_id = customers.id
129+
WHERE
130+
customers.name ILIKE ${`%${query}%`} OR
131+
customers.email ILIKE ${`%${query}%`} OR
132+
invoices.amount::text ILIKE ${`%${query}%`} OR
133+
invoices.date::text ILIKE ${`%${query}%`} OR
134+
invoices.status ILIKE ${`%${query}%`}
135+
`;
136+
137+
const totalPages = Math.ceil(Number(data[0].count) / ITEMS_PER_PAGE);
138+
return totalPages;
139+
} catch (error) {
140+
console.error('Database Error:', error);
141+
throw new Error('Failed to fetch total number of invoices.');
142+
}
143+
}
144+
145+
export async function fetchInvoiceById(id: string) {
146+
try {
147+
const data = await sql<InvoiceForm[]>`
148+
SELECT
149+
invoices.id,
150+
invoices.customer_id,
151+
invoices.amount,
152+
invoices.status
153+
FROM invoices
154+
WHERE invoices.id = ${id};
155+
`;
156+
157+
const invoice = data.map((invoice) => ({
158+
...invoice,
159+
// Convert amount from cents to dollars
160+
amount: invoice.amount / 100,
161+
}));
162+
163+
return invoice[0];
164+
} catch (error) {
165+
console.error('Database Error:', error);
166+
throw new Error('Failed to fetch invoice.');
167+
}
168+
}
169+
170+
export async function fetchCustomers() {
171+
try {
172+
const customers = await sql<CustomerField[]>`
173+
SELECT
174+
id,
175+
name
176+
FROM customers
177+
ORDER BY name ASC
178+
`;
179+
180+
return customers;
181+
} catch (err) {
182+
console.error('Database Error:', err);
183+
throw new Error('Failed to fetch all customers.');
184+
}
185+
}
186+
187+
export async function fetchFilteredCustomers(query: string) {
188+
try {
189+
const data = await sql<CustomersTableType[]>`
190+
SELECT
191+
customers.id,
192+
customers.name,
193+
customers.email,
194+
customers.image_url,
195+
COUNT(invoices.id) AS total_invoices,
196+
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
197+
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
198+
FROM customers
199+
LEFT JOIN invoices ON customers.id = invoices.customer_id
200+
WHERE
201+
customers.name ILIKE ${`%${query}%`} OR
202+
customers.email ILIKE ${`%${query}%`}
203+
GROUP BY customers.id, customers.name, customers.email, customers.image_url
204+
ORDER BY customers.name ASC
205+
`;
206+
207+
const customers = data.map((customer) => ({
208+
...customer,
209+
total_pending: formatCurrency(customer.total_pending),
210+
total_paid: formatCurrency(customer.total_paid),
211+
}));
212+
213+
return customers;
214+
} catch (err) {
215+
console.error('Database Error:', err);
216+
throw new Error('Failed to fetch customer table.');
217+
}
218+
}

app/lib/definitions.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// This file contains type definitions for your data.
2+
// It describes the shape of the data, and what data type each property should accept.
3+
// For simplicity of teaching, we're manually defining these types.
4+
// However, these types are generated automatically if you're using an ORM such as Prisma.
5+
export type User = {
6+
id: string;
7+
name: string;
8+
email: string;
9+
password: string;
10+
};
11+
12+
export type Customer = {
13+
id: string;
14+
name: string;
15+
email: string;
16+
image_url: string;
17+
};
18+
19+
export type Invoice = {
20+
id: string;
21+
customer_id: string;
22+
amount: number;
23+
date: string;
24+
// In TypeScript, this is called a string union type.
25+
// It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
26+
status: 'pending' | 'paid';
27+
};
28+
29+
export type Revenue = {
30+
month: string;
31+
revenue: number;
32+
};
33+
34+
export type LatestInvoice = {
35+
id: string;
36+
name: string;
37+
image_url: string;
38+
email: string;
39+
amount: string;
40+
};
41+
42+
// The database returns a number for amount, but we later format it to a string with the formatCurrency function
43+
export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & {
44+
amount: number;
45+
};
46+
47+
export type InvoicesTable = {
48+
id: string;
49+
customer_id: string;
50+
name: string;
51+
email: string;
52+
image_url: string;
53+
date: string;
54+
amount: number;
55+
status: 'pending' | 'paid';
56+
};
57+
58+
export type CustomersTableType = {
59+
id: string;
60+
name: string;
61+
email: string;
62+
image_url: string;
63+
total_invoices: number;
64+
total_pending: number;
65+
total_paid: number;
66+
};
67+
68+
export type FormattedCustomersTable = {
69+
id: string;
70+
name: string;
71+
email: string;
72+
image_url: string;
73+
total_invoices: number;
74+
total_pending: string;
75+
total_paid: string;
76+
};
77+
78+
export type CustomerField = {
79+
id: string;
80+
name: string;
81+
};
82+
83+
export type InvoiceForm = {
84+
id: string;
85+
customer_id: string;
86+
amount: number;
87+
status: 'pending' | 'paid';
88+
};

0 commit comments

Comments
 (0)