mirror of
https://github.com/vercel/next-learn.git
synced 2026-07-02 08:58:39 +00:00
Update to latest + refactoring.
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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 />}>
|
||||
|
||||
@@ -12,7 +12,6 @@ export default async function Page({
|
||||
| undefined;
|
||||
}) {
|
||||
const query = searchParams?.query || '';
|
||||
|
||||
const customers = await fetchFilteredCustomers(query);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export { default } from 'next-auth/middleware';
|
||||
import { auth } from './auth';
|
||||
|
||||
export const middleware = auth;
|
||||
|
||||
export const config = {
|
||||
matcher: ['/dashboard/:path*'],
|
||||
|
||||
2348
dashboard/15-final/package-lock.json
generated
2348
dashboard/15-final/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user