Skip to content

Commit 9bae365

Browse files
Setup
1 parent 74551fc commit 9bae365

Some content is hidden

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

53 files changed

+9611
-0
lines changed

.env.example

+13
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

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

.gitignore

+36
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

.nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18

app/layout.tsx

+11
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

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

app/lib/definitions.ts

+88
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)