Skip to content

Commit d9ecb01

Browse files
committed
Added initial dashoard
1 parent da2aa7a commit d9ecb01

Some content is hidden

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

59 files changed

+9677
-1
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

.eslintrc.json

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

.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

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
# nextjs-dashboard
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/dashboard/customers/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Customers Page</p>;
3+
}

app/dashboard/invoices/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Invoices Page</p>;
3+
}

app/dashboard/layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SideNav from '@/app/ui/dashboard/sidenav';
2+
3+
export default function Layout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
6+
<div className="w-full flex-none md:w-64">
7+
<SideNav />
8+
</div>
9+
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
10+
</div>
11+
);
12+
}

app/dashboard/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Dashboard Page</p>;
3+
}

app/layout.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import '@/app/ui/global.css';
2+
import { inter } from '@/app/ui/fonts';
3+
4+
export default function RootLayout({
5+
children,
6+
}: {
7+
children: React.ReactNode;
8+
}) {
9+
return (
10+
<html lang="en">
11+
<body className={`${inter.className} antialiased`}>{children}</body>
12+
</html>
13+
);
14+
}

app/lib/data.ts

Lines changed: 231 additions & 0 deletions
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 to 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+
}

0 commit comments

Comments
 (0)