Update to latest + refactoring.

This commit is contained in:
Lee Robinson
2023-10-06 22:13:22 -05:00
parent 4be9de1269
commit 197ffcd6b9
16 changed files with 368 additions and 2491 deletions

View File

@@ -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 };

View File

@@ -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 />}>

View File

@@ -12,7 +12,6 @@ export default async function Page({
| undefined;
}) {
const query = searchParams?.query || '';
const customers = await fetchFilteredCustomers(query);
return (

View File

@@ -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({

View File

@@ -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,
@@ -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) => ({

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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';
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
export async function CardCollected() {
const { numberOfInvoices } = await fetchInvoices();
export default function Card({
return <Card title="Collected">{numberOfInvoices}</Card>;
}
export async function CardPending() {
const { totalPendingInvoices } = await fetchInvoiceStatus();
return <Card title="Pending">{totalPendingInvoices}</Card>;
}
export async function CardTotalInvoices() {
const { totalPaidInvoices } = await fetchInvoiceStatus();
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>
);

View File

@@ -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>

View File

@@ -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';
@@ -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' },
@@ -46,4 +49,6 @@ export const authOptions: NextAuthOptions = {
pages: {
signIn: '/login',
},
};
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig);

View File

@@ -1,4 +1,6 @@
export { default } from 'next-auth/middleware';
import { auth } from './auth';
export const middleware = auth;
export const config = {
matcher: ['/dashboard/:path*'],

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,4 @@
{
"name": "15-final",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
@@ -12,22 +10,22 @@
"dependencies": {
"@heroicons/react": "^2.0.18",
"@tailwindcss/forms": "^0.5.6",
"@types/node": "20.5.7",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@vercel/postgres": "^0.4.1",
"autoprefixer": "10.4.15",
"@types/node": "20.8.3",
"@types/react": "18.2.25",
"@types/react-dom": "18.2.11",
"@vercel/postgres": "^0.5.0",
"autoprefixer": "10.4.16",
"bcrypt": "^5.1.1",
"clsx": "^2.0.0",
"next": "^13.5.3",
"next-auth": "^4.23.1",
"next": "13.5.5-canary.4",
"next-auth": "0.0.0-pr.8775.a98a849e",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"use-debounce": "^9.0.4",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",