Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to latest + refactoring. #204

Closed
wants to merge 15 commits into from
8 changes: 3 additions & 5 deletions dashboard/15-final/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import NextAuth from 'next-auth';
import { authOptions } from '@/auth';
import { handlers } from '@/auth';

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
const { GET, POST } = handlers;
export { GET, POST };
40 changes: 21 additions & 19 deletions dashboard/15-final/app/dashboard/(overview)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import Card from '@/app/ui/dashboard/card';
import { Suspense } from 'react';
import {
CardCollected,
CardPending,
CardTotalInvoices,
CardCustomers,
} from '@/app/ui/dashboard/card';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchCardData } from '@/app/lib/data';
import { Suspense } from 'react';
import {
RevenueChartSkeleton,
LatestInvoicesSkeleton,
CardSkeleton,
} from '@/app/ui/dashboard/skeletons';

export default async function Page() {
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();

export default function Page() {
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
<Suspense fallback={<CardSkeleton />}>
<CardCollected />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<CardPending />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<CardTotalInvoices />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<CardCustomers />
</Suspense>
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}>
Expand Down
1 change: 0 additions & 1 deletion dashboard/15-final/app/dashboard/customers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default async function Page({
| undefined;
}) {
const query = searchParams?.query || '';

const customers = await fetchFilteredCustomers(query);

return (
Expand Down
5 changes: 3 additions & 2 deletions dashboard/15-final/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { inter } from '@/app/ui/fonts';
import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Next.js Dashboard',
description: 'Built as part of nextjs.org/learn.',
metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};

export default function RootLayout({
Expand Down
78 changes: 63 additions & 15 deletions dashboard/15-final/app/lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,54 @@ export async function fetchCardData() {
}
}

export async function fetchInvoices() {
try {
const data = await sql`SELECT COUNT(*) FROM invoices`;
const numberOfInvoices = Number(data.rows[0].count ?? '0');

return {
numberOfInvoices,
};
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to card data.');
}
}

export async function fetchCustomers() {
try {
const data = await sql`SELECT COUNT(*) FROM customers`;
const numberOfCustomers = Number(data.rows[0].count ?? '0');

return {
numberOfCustomers,
};
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to card data.');
}
}

export async function fetchInvoiceStatus() {
try {
const data = await 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 totalPaidInvoices = formatCurrency(data.rows[0].paid ?? '0');
const totalPendingInvoices = formatCurrency(data.rows[0].pending ?? '0');

return {
totalPaidInvoices,
totalPendingInvoices,
};
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to card data.');
}
}

export async function fetchFilteredInvoices(
query: string,
currentPage: number,
Expand Down Expand Up @@ -210,21 +258,21 @@ export async function fetchCustomersTable() {
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
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) => ({
Expand Down
6 changes: 3 additions & 3 deletions dashboard/15-final/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import LoginForm from '@/app/ui/login-form';
import { authOptions } from '@/auth';
import { getServerSession } from 'next-auth';
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function Page() {
const session = await getServerSession(authOptions);
const session = await auth();
if (session) redirect('/dashboard');

return (
<main>
<LoginForm />
Expand Down
1 change: 1 addition & 0 deletions dashboard/15-final/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ArrowRightIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image';
import Link from 'next/link';

export default function Page() {
return (
<main className="flex min-h-screen flex-col p-6">
Expand Down
55 changes: 41 additions & 14 deletions dashboard/15-final/app/ui/dashboard/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,63 @@ import {
InboxIcon,
} from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import {
fetchCustomers,
fetchInvoices,
fetchInvoiceStatus,
} from '@/app/lib/data';

export async function CardCollected() {
const { numberOfInvoices } = await fetchInvoices();

return <Card title="Collected">{numberOfInvoices}</Card>;
}

export async function CardPending() {
const { totalPendingInvoices } = await fetchInvoiceStatus();

return <Card title="Pending">{totalPendingInvoices}</Card>;
}

const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
export async function CardTotalInvoices() {
const { totalPaidInvoices } = await fetchInvoiceStatus();

export default function Card({
return <Card title="Invoices">{totalPaidInvoices}</Card>;
}

export async function CardCustomers() {
const { numberOfCustomers } = await fetchCustomers();

return <Card title="Customers">{numberOfCustomers}</Card>;
}

function Card({
title,
value,
type,
children,
}: {
title: string;
value: number | string;
type: 'invoices' | 'customers' | 'pending' | 'collected';
children: React.ReactNode;
}) {
const Icon = iconMap[type];
const icons = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};

const Icon = icons[title.toLowerCase()];

return (
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
<div className="flex p-4">
{Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
<Icon className="h-5 w-5 text-gray-700" />
<h3 className="ml-2 text-sm font-medium">{title}</h3>
</div>
<p
className={`${lusitana.className}
truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
>
{value}
{children}
</p>
</div>
);
Expand Down
4 changes: 1 addition & 3 deletions dashboard/15-final/app/ui/invoices/create-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ export default function Form({
placeholder="Enter USD amount"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
aria-describedby="amount-error"
style={
{ '-moz-appearance': 'textfield' } as React.CSSProperties
}
style={{ MozAppearance: 'textfield' } as React.CSSProperties}
/>
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
Expand Down
15 changes: 10 additions & 5 deletions dashboard/15-final/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import NextAuth from 'next-auth';
import type { NextAuthConfig } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import GitHub from 'next-auth/providers/github';
import bcrypt from 'bcrypt';
import { User } from '@/app/lib/definitions';
import { sql } from '@vercel/postgres';
Expand All @@ -14,9 +16,10 @@ async function getUser(email: string) {
}
}

export const authOptions: NextAuthOptions = {
export const authConfig = {
providers: [
CredentialsProvider({
GitHub,
Credentials({
name: 'Sign-In with Credentials',
credentials: {
password: { label: 'Password', type: 'password' },
Expand Down Expand Up @@ -46,4 +49,6 @@ export const authOptions: NextAuthOptions = {
pages: {
signIn: '/login',
},
};
} satisfies NextAuthConfig;

export const { handlers, auth, signIn, signOut } = NextAuth(authConfig);
4 changes: 3 additions & 1 deletion dashboard/15-final/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default } from 'next-auth/middleware';
import { auth } from './auth';

export const middleware = auth;

export const config = {
matcher: ['/dashboard/:path*'],
Expand Down
Loading
Loading