Add starter example (#225)

* Duplicate starter

* Remove code for chapter 16

* Add code for chapter 15

* first 3 chapters

* Remove routes and actions

* chapter 3

* Chapters 12-13

* chapter 5

* Revert "Chapters 12-13"

This reverts commit b6da764d85.

* re-add Link to page

* chapter 5

* chapter 6

* Chapter 11 and 12

* chapter 7

* Revert

* Chapter 11

* Remove PPR flag

* chapter 8

* Chapter 9

* switch from pnpm to npm

* 💅

* Create pnpm-lock.yaml

* build errors

* Fix

* Fix

* Update next

* Update nextauth

---------

Co-authored-by: StephDietz <steph.dietz@vercel.com>
This commit is contained in:
Delba de Oliveira
2023-10-26 18:49:57 +01:00
committed by GitHub
parent 9044c85918
commit 82f2fae791
60 changed files with 12188 additions and 154 deletions

View File

@@ -31,7 +31,7 @@ export type State = {
amount?: string[];
status?: string[];
};
message: string;
message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {

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

@@ -8,13 +8,12 @@ import {
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '../button';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
// @ts-ignore React types do not yet include useFormState
import { experimental_useFormState as useFormState } from 'react-dom';
import { useFormState } from 'react-dom';
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: [] };
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return (

View File

@@ -10,8 +10,7 @@ import {
import Link from 'next/link';
import { Button } from '../button';
import { updateInvoice } from '@/app/lib/actions';
// @ts-ignore React types do not yet include useFormState
import { experimental_useFormState as useFormState } from 'react-dom';
import { useFormState } from 'react-dom';
export default function EditInvoiceForm({
invoice,
@@ -20,7 +19,7 @@ export default function EditInvoiceForm({
invoice: InvoiceForm;
customers: CustomerField[];
}) {
const initialState = { message: null, errors: [] };
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(updateInvoice, initialState);
return (

View File

@@ -62,9 +62,7 @@ export default function LoginForm() {
</div>
</div>
</div>
<Button className="mt-4 w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
<LoginButton />
<div className="flex h-8 items-end space-x-1">
{code === 'CredentialsSignin' && (
<>

2282
dashboard/final-example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,8 @@
"autoprefixer": "10.4.15",
"bcrypt": "^5.1.1",
"clsx": "^2.0.0",
"next": "13.5.7-canary.27",
"next-auth": "5.0.0-beta.2",
"next": "^14.0.0",
"next-auth": "^5.0.0-beta.3",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",

36
dashboard/starter-example/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1 @@
18

View File

@@ -0,0 +1,5 @@
## Next.js App Router Course - Final
This is the final template for the Next.js App Router Course. It contains the final code for the dashboard application.
For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.

View File

@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,231 @@
import { sql } from '@vercel/postgres';
import {
CustomerField,
CustomersTable,
InvoiceForm,
InvoicesTable,
LatestInvoiceRaw,
User,
Revenue,
} from './definitions';
import { formatCurrency } from './utils';
export async function fetchRevenue() {
// Add noStore() here prevent the response from being cached.
// This is equivalent to in fetch(..., {cache: 'no-store'}).
try {
// Artificially delay a reponse for demo purposes.
// Don't do this in real life :)
// console.log('Fetching revenue data...');
// await new Promise((resolve) => setTimeout(resolve, 3000));
const data = await sql<Revenue>`SELECT * FROM revenue`;
// console.log('Data fetch complete after 3 seconds.');
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch revenue data.');
}
}
export async function fetchLatestInvoices() {
try {
const data = await sql<LatestInvoiceRaw>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;
const latestInvoices = data.rows.map((invoice) => ({
...invoice,
amount: formatCurrency(invoice.amount),
}));
return latestInvoices;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}
export async function fetchCardData() {
try {
// You can probably combine these into a single SQL query
// However, we are intentionally splitting them to demonstrate
// how to initialize multiple queries in parallel with JS.
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = 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 data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
const numberOfInvoices = Number(data[0].rows[0].count ?? '0');
const numberOfCustomers = Number(data[1].rows[0].count ?? '0');
const totalPaidInvoices = formatCurrency(data[2].rows[0].paid ?? '0');
const totalPendingInvoices = formatCurrency(data[2].rows[0].pending ?? '0');
return {
numberOfCustomers,
numberOfInvoices,
totalPaidInvoices,
totalPendingInvoices,
};
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to card data.');
}
}
const ITEMS_PER_PAGE = 6;
export async function fetchFilteredInvoices(
query: string,
currentPage: number,
) {
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
try {
const invoices = await sql<InvoicesTable>`
SELECT
invoices.id,
invoices.amount,
invoices.date,
invoices.status,
customers.name,
customers.email,
customers.image_url
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
ORDER BY invoices.date DESC
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
`;
return invoices.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch invoices.');
}
}
export async function fetchInvoicesPages(query: string) {
try {
const count = await sql`SELECT COUNT(*)
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
`;
const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE);
return totalPages;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch total number of invoices.');
}
}
export async function fetchInvoiceById(id: string) {
try {
const data = await sql<InvoiceForm>`
SELECT
invoices.id,
invoices.customer_id,
invoices.amount,
invoices.status
FROM invoices
WHERE invoices.id = ${id};
`;
const invoice = data.rows.map((invoice) => ({
...invoice,
// Convert amount from cents to dollars
amount: invoice.amount / 100,
}));
return invoice[0];
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch invoice.');
}
}
export async function fetchCustomers() {
try {
const data = await sql<CustomerField>`
SELECT
id,
name
FROM customers
ORDER BY name ASC
`;
const customers = data.rows;
return customers;
} catch (err) {
console.error('Database Error:', err);
throw new Error('Failed to fetch all customers.');
}
}
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
`;
const customers = data.rows.map((customer) => ({
...customer,
total_pending: formatCurrency(customer.total_pending),
total_paid: formatCurrency(customer.total_paid),
}));
return customers;
} catch (err) {
console.error('Database Error:', err);
throw new Error('Failed to fetch customer table.');
}
}
export async function getUser(email: string) {
try {
const user = await sql`SELECT * from USERS where email=${email}`;
return user.rows[0] as User;
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}

View File

@@ -0,0 +1,88 @@
// This file contains type definitions for your data.
// It describes the shape of the data, and what data type each property should accept.
// For simplicity of teaching, we're manually defining these types.
// However, these types are generated automatically if you're using an ORM such as Prisma.
export type User = {
id: string;
name: string;
email: string;
password: string;
};
export type Customer = {
id: string;
name: string;
email: string;
image_url: string;
};
export type Invoice = {
id: string;
customer_id: string;
amount: number;
date: string;
// In TypeScript, this is called a string union type.
// It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
status: 'pending' | 'paid';
};
export type Revenue = {
month: string;
revenue: number;
};
export type LatestInvoice = {
id: string;
name: string;
image_url: string;
email: string;
amount: string;
};
// The database returns a number for amount, but we later format it to a string with the formatCurrency function
export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & {
amount: number;
};
export type InvoicesTable = {
id: string;
customer_id: string;
name: string;
email: string;
image_url: string;
date: string;
amount: number;
status: 'pending' | 'paid';
};
export type CustomersTable = {
id: string;
name: string;
email: string;
image_url: string;
total_invoices: number;
total_pending: number;
total_paid: number;
};
export type FormattedCustomersTable = {
id: string;
name: string;
email: string;
image_url: string;
total_invoices: number;
total_pending: string;
total_paid: string;
};
export type CustomerField = {
id: string;
name: string;
};
export type InvoiceForm = {
id: string;
customer_id: string;
amount: number;
status: 'pending' | 'paid';
};

View File

@@ -0,0 +1,188 @@
// This file contains placeholder data that you'll be replacing with real data in the Data Fetching chapter:
// https://nextjs.org/learn/dashboard-app/fetching-data
const users = [
{
id: '410544b2-4001-4271-9855-fec4b6a6442a',
name: 'User',
email: 'user@nextmail.com',
password: '123456',
},
];
const customers = [
{
id: '3958dc9e-712f-4377-85e9-fec4b6a6442a',
name: 'Delba de Oliveira',
email: 'delba@oliveira.com',
image_url: '/customers/delba-de-oliveira.png',
},
{
id: '3958dc9e-742f-4377-85e9-fec4b6a6442a',
name: 'Lee Robinson',
email: 'lee@robinson.com',
image_url: '/customers/lee-robinson.png',
},
{
id: '3958dc9e-737f-4377-85e9-fec4b6a6442a',
name: 'Hector Simpson',
email: 'hector@simpson.com',
image_url: '/customers/hector-simpson.png',
},
{
id: '50ca3e18-62cd-11ee-8c99-0242ac120002',
name: 'Steven Tey',
email: 'steven@tey.com',
image_url: '/customers/steven-tey.png',
},
{
id: '3958dc9e-787f-4377-85e9-fec4b6a6442a',
name: 'Steph Dietz',
email: 'steph@dietz.com',
image_url: '/customers/steph-dietz.png',
},
{
id: '76d65c26-f784-44a2-ac19-586678f7c2f2',
name: 'Michael Novotny',
email: 'michael@novotny.com',
image_url: '/customers/michael-novotny.png',
},
{
id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa',
name: 'Evil Rabbit',
email: 'evil@rabbit.com',
image_url: '/customers/evil-rabbit.png',
},
{
id: '126eed9c-c90c-4ef6-a4a8-fcf7408d3c66',
name: 'Emil Kowalski',
email: 'emil@kowalski.com',
image_url: '/customers/emil-kowalski.png',
},
{
id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9',
name: 'Amy Burns',
email: 'amy.burns@vercel.com',
image_url: '/customers/amy-burns.png',
},
{
id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB',
name: 'Balazs Orban',
email: 'balazs.orban@vercel.com',
image_url: '/customers/balazs-orban.png',
},
];
const invoices = [
{
customer_id: customers[0].id,
amount: 15795,
status: 'pending',
date: '2022-12-06',
},
{
customer_id: customers[1].id,
amount: 20348,
status: 'pending',
date: '2022-11-14',
},
{
customer_id: customers[4].id,
amount: 3040,
status: 'paid',
date: '2022-10-29',
},
{
customer_id: customers[3].id,
amount: 44800,
status: 'paid',
date: '2023-09-10',
},
{
customer_id: customers[5].id,
amount: 34577,
status: 'pending',
date: '2023-08-05',
},
{
customer_id: customers[7].id,
amount: 54246,
status: 'pending',
date: '2023-07-16',
},
{
customer_id: customers[6].id,
amount: 666,
status: 'pending',
date: '2023-06-27',
},
{
customer_id: customers[3].id,
amount: 32545,
status: 'paid',
date: '2023-06-09',
},
{
customer_id: customers[4].id,
amount: 1250,
status: 'paid',
date: '2023-06-17',
},
{
customer_id: customers[5].id,
amount: 8546,
status: 'paid',
date: '2023-06-07',
},
{
customer_id: customers[1].id,
amount: 500,
status: 'paid',
date: '2023-08-19',
},
{
customer_id: customers[5].id,
amount: 8945,
status: 'paid',
date: '2023-06-03',
},
{
customer_id: customers[2].id,
amount: 8945,
status: 'paid',
date: '2023-06-18',
},
{
customer_id: customers[0].id,
amount: 8945,
status: 'paid',
date: '2023-10-04',
},
{
customer_id: customers[2].id,
amount: 1000,
status: 'paid',
date: '2022-06-05',
},
];
const revenue = [
{ month: 'Jan', revenue: 2000 },
{ month: 'Feb', revenue: 1800 },
{ month: 'Mar', revenue: 2200 },
{ month: 'Apr', revenue: 2500 },
{ month: 'May', revenue: 2300 },
{ month: 'Jun', revenue: 3200 },
{ month: 'Jul', revenue: 3500 },
{ month: 'Aug', revenue: 3700 },
{ month: 'Sep', revenue: 2500 },
{ month: 'Oct', revenue: 2800 },
{ month: 'Nov', revenue: 3000 },
{ month: 'Dec', revenue: 4800 },
];
module.exports = {
users,
customers,
invoices,
revenue,
};

View File

@@ -0,0 +1,69 @@
import { Revenue } from './definitions';
export const formatCurrency = (amount: number) => {
return (amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});
};
export const formatDateToLocal = (
dateStr: string,
locale: string = 'en-US',
) => {
const date = new Date(dateStr);
const options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'short',
year: 'numeric',
};
const formatter = new Intl.DateTimeFormat(locale, options);
return formatter.format(date);
};
export const generateYAxis = (revenue: Revenue[]) => {
// Calculate what labels we need to display on the y-axis
// based on highest record and in 1000s
const yAxisLabels = [];
const highestRecord = Math.max(...revenue.map((month) => month.revenue));
const topLabel = Math.ceil(highestRecord / 1000) * 1000;
for (let i = topLabel; i >= 0; i -= 1000) {
yAxisLabels.push(`$${i / 1000}K`);
}
return { yAxisLabels, topLabel };
};
export const generatePagination = (currentPage: number, totalPages: number) => {
// If the total number of pages is 7 or less,
// display all pages without any ellipsis.
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
// If the current page is among the first 3 pages,
// show the first 3, an ellipsis, and the last 2 pages.
if (currentPage <= 3) {
return [1, 2, 3, '...', totalPages - 1, totalPages];
}
// If the current page is among the last 3 pages,
// show the first 2, an ellipsis, and the last 3 pages.
if (currentPage >= totalPages - 2) {
return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
}
// If the current page is somewhere in the middle,
// show the first page, an ellipsis, the current page and its neighbors,
// another ellipsis, and the last page.
return [
1,
'...',
currentPage - 1,
currentPage,
currentPage + 1,
'...',
totalPages,
];
};

View File

@@ -0,0 +1,31 @@
import AcmeLogo from '@/app/ui/acme-logo';
import Link from 'next/link';
export default function Page() {
return (
<main className="flex min-h-screen flex-col p-6">
<div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
{/* <AcmeLogo /> */}
</div>
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
<p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}>
<strong>Welcome to Acme.</strong> This is the example for the{' '}
<a href="https://nextjs.org/learn/" className="text-blue-500">
Next.js Learn Course
</a>
, brought to you by Vercel.
</p>
<Link
href="/login"
className="flex items-center gap-5 self-start rounded-lg bg-blue-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-blue-400 md:text-base"
>
<span>Log in</span>
</Link>
</div>
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
{/* Add Hero Images Here */}
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,13 @@
import { GlobeAltIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
export default function AcmeLogo() {
return (
<div
className={`${lusitana.className} flex flex-row items-center leading-none text-white`}
>
<GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
<p className="text-[44px]">Acme</p>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import clsx from 'clsx';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function Button({ children, className, ...rest }: ButtonProps) {
return (
<button
{...rest}
className={clsx(
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
className,
)}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,120 @@
import Image from 'next/image';
import { lusitana } from '@/app/ui/fonts';
import Search from '../search';
import { CustomersTable, FormattedCustomersTable } from '@/app/lib/definitions';
export default async function CustomersTable({
customers,
}: {
customers: FormattedCustomersTable[];
}) {
return (
<div className="w-full">
<h1 className={`${lusitana.className} mb-8 text-xl md:text-2xl`}>
Customers
</h1>
<Search placeholder="Search customers..." />
<div className="mt-6 flow-root">
<div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle">
<div className="overflow-hidden rounded-md bg-gray-50 p-2 md:pt-0">
<div className="md:hidden">
{customers?.map((customer) => (
<div
key={customer.id}
className="mb-2 w-full rounded-md bg-white p-4"
>
<div className="flex items-center justify-between border-b pb-4">
<div>
<div className="mb-2 flex items-center">
<div className="flex items-center gap-3">
<Image
src={customer.image_url}
className="rounded-full"
alt={`${customer.name}'s profile picture`}
width={28}
height={28}
/>
<p>{customer.name}</p>
</div>
</div>
<p className="text-sm text-gray-500">
{customer.email}
</p>
</div>
</div>
<div className="flex w-full items-center justify-between border-b py-5">
<div className="flex w-1/2 flex-col">
<p className="text-xs">Pending</p>
<p className="font-medium">{customer.total_pending}</p>
</div>
<div className="flex w-1/2 flex-col">
<p className="text-xs">Paid</p>
<p className="font-medium">{customer.total_paid}</p>
</div>
</div>
<div className="pt-4 text-sm">
<p>{customer.total_invoices} invoices</p>
</div>
</div>
))}
</div>
<table className="hidden min-w-full rounded-md text-gray-900 md:table">
<thead className="rounded-md bg-gray-50 text-left text-sm font-normal">
<tr>
<th scope="col" className="px-4 py-5 font-medium sm:pl-6">
Name
</th>
<th scope="col" className="px-3 py-5 font-medium">
Email
</th>
<th scope="col" className="px-3 py-5 font-medium">
Total Invoices
</th>
<th scope="col" className="px-3 py-5 font-medium">
Total Pending
</th>
<th scope="col" className="px-4 py-5 font-medium">
Total Paid
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-900">
{customers.map((customer) => (
<tr key={customer.id} className="group">
<td className="whitespace-nowrap bg-white py-5 pl-4 pr-3 text-sm text-black group-first-of-type:rounded-md group-last-of-type:rounded-md sm:pl-6">
<div className="flex items-center gap-3">
<Image
src={customer.image_url}
className="rounded-full"
alt={`${customer.name}'s profile picture`}
width={28}
height={28}
/>
<p>{customer.name}</p>
</div>
</td>
<td className="whitespace-nowrap bg-white px-4 py-5 text-sm">
{customer.email}
</td>
<td className="whitespace-nowrap bg-white px-4 py-5 text-sm">
{customer.total_invoices}
</td>
<td className="whitespace-nowrap bg-white px-4 py-5 text-sm">
{customer.total_pending}
</td>
<td className="whitespace-nowrap bg-white px-4 py-5 text-sm group-first-of-type:rounded-md group-last-of-type:rounded-md">
{customer.total_paid}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import {
BanknotesIcon,
ClockIcon,
UserGroupIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
export default async function Cards() {
return (
<>
<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"
/>
</>
);
}
export function Card({
title,
value,
type,
}: {
title: string;
value: number | string;
type: 'invoices' | 'customers' | 'pending' | 'collected';
}) {
const Icon = iconMap[type];
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}
<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}
</p>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Image from 'next/image';
import { lusitana } from '@/app/ui/fonts';
export default async function LatestInvoices() {
return (
<div className="flex w-full flex-col md:col-span-4 lg:col-span-4">
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Latest Invoices
</h2>
<div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
<div className="bg-white px-6">
{latestInvoices.map((invoice, i) => {
return (
<div
key={invoice.id}
className={clsx(
'flex flex-row items-center justify-between py-4',
{
'border-t': i !== 0,
},
)}
>
<div className="flex items-center">
<Image
src={invoice.image_url}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.name}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.email}
</p>
</div>
</div>
<p
className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
>
{invoice.amount}
</p>
</div>
);
})}
</div>
<div className="flex items-center pb-2 pt-6">
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import clsx from 'clsx';
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{
name: 'Invoices',
href: '/dashboard/invoices',
icon: DocumentDuplicateIcon,
},
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
];
export default function NavLinks() {
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<a
key={link.name}
href={link.href}
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</a>
);
})}
</>
);
}

View File

@@ -0,0 +1,59 @@
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
// This component is representational only.
// For data visualization UI, check out:
// https://www.tremor.so/
// https://www.chartjs.org/
// https://airbnb.io/visx/
export default async function RevenueChart() {
const chartHeight = 350;
const { yAxisLabels, topLabel } = generateYAxis(revenue);
if (!revenue || revenue.length === 0) {
return <p className="mt-4 text-gray-400">No data available.</p>;
}
return (
<div className="w-full md:col-span-4">
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Recent Revenue
</h2>
<div className="rounded-xl bg-gray-50 p-4">
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
{/* y-axis */}
<div
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
style={{ height: `${chartHeight}px` }}
>
{yAxisLabels.map((label) => (
<p key={label}>{label}</p>
))}
</div>
{revenue.map((month) => (
<div key={month.month} className="flex flex-col items-center gap-2">
{/* bars */}
<div
className="w-full rounded-md bg-blue-300"
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}
></div>
{/* x-axis */}
<p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
{month.month}
</p>
</div>
))}
</div>
<div className="flex items-center pb-2 pt-6">
<CalendarIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Last 12 months</h3>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
import AcmeLogo from '../acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
import { signOut } from '@/auth';
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40">
<AcmeLogo />
</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form
action={async () => {
'use server';
await signOut();
}}
>
<button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
</button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,218 @@
// Loading animation
const shimmer =
'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent';
export function CardSkeleton() {
return (
<div
className={`${shimmer} relative overflow-hidden rounded-xl bg-gray-100 p-2 shadow-sm`}
>
<div className="flex p-4">
<div className="h-5 w-5 rounded-md bg-gray-200" />
<div className="ml-2 h-6 w-16 rounded-md bg-gray-200 text-sm font-medium" />
</div>
<div className="flex items-center justify-center truncate rounded-xl bg-white px-4 py-8">
<div className="h-7 w-20 rounded-md bg-gray-200" />
</div>
</div>
);
}
export function CardsSkeleton() {
return (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
);
}
export function RevenueChartSkeleton() {
return (
<div className={`${shimmer} relative w-full overflow-hidden md:col-span-4`}>
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
<div className="rounded-xl bg-gray-100 p-4">
<div className="sm:grid-cols-13 mt-0 grid h-[410px] grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4" />
<div className="flex items-center pb-2 pt-6">
<div className="h-5 w-5 rounded-full bg-gray-200" />
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
</div>
</div>
</div>
);
}
export function InvoiceSkeleton() {
return (
<div className="flex flex-row items-center justify-between border-b border-gray-100 py-4">
<div className="flex items-center">
<div className="mr-2 h-8 w-8 rounded-full bg-gray-200" />
<div className="min-w-0">
<div className="h-5 w-40 rounded-md bg-gray-200" />
<div className="mt-2 h-4 w-12 rounded-md bg-gray-200" />
</div>
</div>
<div className="mt-2 h-4 w-12 rounded-md bg-gray-200" />
</div>
);
}
export function LatestInvoicesSkeleton() {
return (
<div
className={`${shimmer} relative flex w-full flex-col overflow-hidden md:col-span-4 lg:col-span-4`}
>
<div className="mb-4 h-8 w-36 rounded-md bg-gray-100" />
<div className="flex grow flex-col justify-between rounded-xl bg-gray-100 p-4">
<div className="bg-white px-6">
<InvoiceSkeleton />
<InvoiceSkeleton />
<InvoiceSkeleton />
<InvoiceSkeleton />
<InvoiceSkeleton />
<div className="flex items-center pb-2 pt-6">
<div className="h-5 w-5 rounded-full bg-gray-200" />
<div className="ml-2 h-4 w-20 rounded-md bg-gray-200" />
</div>
</div>
</div>
</div>
);
}
export default function DashboardSkeleton() {
return (
<>
<div
className={`${shimmer} relative mb-4 h-8 w-36 overflow-hidden rounded-md bg-gray-100`}
/>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<RevenueChartSkeleton />
<LatestInvoicesSkeleton />
</div>
</>
);
}
export function TableRowSkeleton() {
return (
<tr className="w-full border-b border-gray-100 last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg">
{/* Customer Name and Image */}
<td className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-gray-100"></div>
<div className="h-6 w-24 rounded bg-gray-100"></div>
</div>
</td>
{/* Email */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-32 rounded bg-gray-100"></div>
</td>
{/* Amount */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
</td>
{/* Date */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
</td>
{/* Status */}
<td className="whitespace-nowrap px-3 py-3">
<div className="h-6 w-16 rounded bg-gray-100"></div>
</td>
{/* Actions */}
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex justify-end gap-3">
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
<div className="h-[38px] w-[38px] rounded bg-gray-100"></div>
</div>
</td>
</tr>
);
}
export function InvoicesMobileSkeleton() {
return (
<div className="mb-2 w-full rounded-md bg-white p-4">
<div className="flex items-center justify-between border-b border-gray-100 pb-8">
<div className="flex items-center">
<div className="mr-2 h-8 w-8 rounded-full bg-gray-100"></div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
</div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
</div>
<div className="flex w-full items-center justify-between pt-4">
<div>
<div className="h-6 w-16 rounded bg-gray-100"></div>
<div className="mt-2 h-6 w-24 rounded bg-gray-100"></div>
</div>
<div className="flex justify-end gap-2">
<div className="h-10 w-10 rounded bg-gray-100"></div>
<div className="h-10 w-10 rounded bg-gray-100"></div>
</div>
</div>
</div>
);
}
export function InvoicesTableSkeleton() {
return (
<div className="mt-6 flow-root">
<div className="inline-block min-w-full align-middle">
<div className="rounded-lg bg-gray-50 p-2 md:pt-0">
<div className="md:hidden">
<InvoicesMobileSkeleton />
<InvoicesMobileSkeleton />
<InvoicesMobileSkeleton />
<InvoicesMobileSkeleton />
<InvoicesMobileSkeleton />
<InvoicesMobileSkeleton />
</div>
<table className="hidden min-w-full text-gray-900 md:table">
<thead className="rounded-lg text-left text-sm font-normal">
<tr>
<th scope="col" className="px-4 py-5 font-medium sm:pl-6">
Customer
</th>
<th scope="col" className="px-3 py-5 font-medium">
Email
</th>
<th scope="col" className="px-3 py-5 font-medium">
Amount
</th>
<th scope="col" className="px-3 py-5 font-medium">
Date
</th>
<th scope="col" className="px-3 py-5 font-medium">
Status
</th>
<th
scope="col"
className="relative pb-4 pl-3 pr-6 pt-2 sm:pr-6"
>
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white">
<TableRowSkeleton />
<TableRowSkeleton />
<TableRowSkeleton />
<TableRowSkeleton />
<TableRowSkeleton />
<TableRowSkeleton />
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
input[type='number'] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@@ -0,0 +1,36 @@
import { clsx } from 'clsx';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
interface Breadcrumb {
label: string;
href: string;
active?: boolean;
}
export default function Breadcrumbs({
breadcrumbs,
}: {
breadcrumbs: Breadcrumb[];
}) {
return (
<nav aria-label="Breadcrumb" className="mb-6 block">
<ol className={clsx(lusitana.className, 'flex text-xl md:text-2xl')}>
{breadcrumbs.map((breadcrumb, index) => (
<li
key={breadcrumb.href}
aria-current={breadcrumb.active}
className={clsx(
breadcrumb.active ? 'text-gray-900' : 'text-gray-500',
)}
>
<Link href={breadcrumb.href}>{breadcrumb.label}</Link>
{index < breadcrumbs.length - 1 ? (
<span className="mx-3 inline-block">/</span>
) : null}
</li>
))}
</ol>
</nav>
);
}

View File

@@ -0,0 +1,38 @@
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { deleteInvoice } from '@/app/lib/actions';
export function CreateInvoice() {
return (
<Link
href="/dashboard/invoices/create"
className="flex h-10 items-center rounded-lg bg-blue-600 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
<span className="hidden md:block">Create Invoice</span>{' '}
<PlusIcon className="h-5 md:ml-4" />
</Link>
);
}
export function UpdateInvoice({ id }: { id: string }) {
return (
<Link
href={`/dashboard/invoices/${id}/edit`}
className="rounded-md border p-2 hover:bg-gray-100"
>
<PencilIcon className="w-5" />
</Link>
);
}
export function DeleteInvoice({ id }: { id: string }) {
return (
<form action={deleteInvoice}>
<input type="hidden" name="id" value={id} />
<button className="rounded-md border p-2 hover:bg-gray-100">
<span className="sr-only">Delete</span>
<TrashIcon className="w-5" />
</button>
</form>
);
}

View File

@@ -0,0 +1,115 @@
'use client';
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '../button';
export default function Form({ customers }: { customers: CustomerField[] }) {
return (
<form>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
Choose customer
</label>
<div className="relative">
<select
id="customer"
name="customerId"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
>
<option value="" disabled>
Select a customer
</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
</div>
{/* Invoice Amount */}
<div className="mb-4">
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
Choose an amount
</label>
<div className="relative mt-2 rounded-md">
<div className="relative">
<input
id="amount"
name="amount"
type="number"
step="0.01"
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"
/>
<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>
</div>
s
</div>
{/* Invoice Status */}
<div>
<label htmlFor="status" className="mb-2 block text-sm font-medium">
Set the invoice status
</label>
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
<div className="flex gap-4">
<div className="flex items-center">
<input
id="pending"
name="status"
type="radio"
value="pending"
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
htmlFor="pending"
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
>
Pending <ClockIcon className="h-4 w-4" />
</label>
</div>
<div className="flex items-center">
<input
id="paid"
name="status"
type="radio"
value="paid"
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
htmlFor="paid"
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
>
Paid <CheckIcon className="h-4 w-4" />
</label>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end gap-4">
<Link
href="/dashboard/invoices"
className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
>
Cancel
</Link>
<Button type="submit">Create Invoice</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,124 @@
'use client';
import { CustomerField, InvoiceForm } from '@/app/lib/definitions';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { Button } from '../button';
export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm;
customers: CustomerField[];
}) {
return (
<form>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Invoice ID */}
<input type="hidden" name="id" value={invoice.id} />
{/* Customer Name */}
<div className="mb-4">
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
Choose customer
</label>
<div className="relative">
<select
id="customer"
name="customerId"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue={invoice.customer_id}
>
<option value="" disabled>
Select a customer
</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
</div>
{/* Invoice Amount */}
<div className="mb-4">
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
Choose an amount
</label>
<div className="relative mt-2 rounded-md">
<div className="relative">
<input
id="amount"
name="amount"
type="number"
defaultValue={invoice.amount}
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"
/>
<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>
</div>
</div>
{/* Invoice Status */}
<div>
<label htmlFor="status" className="mb-2 block text-sm font-medium">
Set the invoice status
</label>
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
<div className="flex gap-4">
<div className="flex items-center">
<input
id="pending"
name="status"
type="radio"
value="pending"
defaultChecked={invoice.status === 'pending'}
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
htmlFor="pending"
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
>
Pending <ClockIcon className="h-4 w-4" />
</label>
</div>
<div className="flex items-center">
<input
id="paid"
name="status"
type="radio"
value="paid"
defaultChecked={invoice.status === 'paid'}
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
htmlFor="paid"
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
>
Paid <CheckIcon className="h-4 w-4" />
</label>
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 flex justify-end gap-4">
<Link
href="/dashboard/invoices"
className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
>
Cancel
</Link>
<Button type="submit">Edit Invoice</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,113 @@
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
export default function Pagination({ totalPages }: { totalPages: number }) {
const allPages = generatePagination(currentPage, totalPages);
return (
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: 'first' | 'last' | 'single' | 'middle' | undefined;
if (index === 0) position = 'first';
if (index === allPages.length - 1) position = 'last';
if (allPages.length === 1) position = 'single';
if (page === '...') position = 'middle';
return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>
<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div>
);
}
function PaginationNumber({
page,
href,
isActive,
position,
}: {
page: number | string;
href: string;
position?: 'first' | 'last' | 'middle' | 'single';
isActive: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center text-sm border',
{
'rounded-l-md': position === 'first' || position === 'single',
'rounded-r-md': position === 'last' || position === 'single',
'z-10 bg-blue-600 border-blue-600 text-white': isActive,
'hover:bg-gray-100': !isActive && position !== 'middle',
'text-gray-300': position === 'middle',
},
);
return isActive || position === 'middle' ? (
<div className={className}>{page}</div>
) : (
<Link href={href} className={className}>
{page}
</Link>
);
}
function PaginationArrow({
href,
direction,
isDisabled,
}: {
href: string;
direction: 'left' | 'right';
isDisabled?: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center rounded-md border',
{
'pointer-events-none text-gray-300': isDisabled,
'hover:bg-gray-100': !isDisabled,
'mr-2 md:mr-4': direction === 'left',
'ml-2 md:ml-4': direction === 'right',
},
);
const icon =
direction === 'left' ? (
<ArrowLeftIcon className="w-4" />
) : (
<ArrowRightIcon className="w-4" />
);
return isDisabled ? (
<div className={className}>{icon}</div>
) : (
<Link className={className} href={href}>
{icon}
</Link>
);
}

View File

@@ -0,0 +1,29 @@
import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
export default function InvoiceStatus({ status }: { status: string }) {
return (
<span
className={clsx(
'inline-flex items-center rounded-full px-2 py-1 text-xs',
{
'bg-gray-100 text-gray-500': status === 'pending',
'bg-green-500 text-white': status === 'paid',
},
)}
>
{status === 'pending' ? (
<>
Pending
<ClockIcon className="ml-1 w-4 text-gray-500" />
</>
) : null}
{status === 'paid' ? (
<>
Paid
<CheckIcon className="ml-1 w-4 text-white" />
</>
) : null}
</span>
);
}

View File

@@ -0,0 +1,124 @@
import Image from 'next/image';
import { UpdateInvoice, DeleteInvoice } from '@/app/ui/invoices/buttons';
import InvoiceStatus from '@/app/ui/invoices/status';
import { formatDateToLocal, formatCurrency } from '@/app/lib/utils';
import { fetchFilteredInvoices } from '@/app/lib/data';
export default async function InvoicesTable({
query,
currentPage,
}: {
query: string;
currentPage: number;
}) {
const invoices = await fetchFilteredInvoices(query, currentPage);
return (
<div className="mt-6 flow-root">
<div className="inline-block min-w-full align-middle">
<div className="rounded-lg bg-gray-50 p-2 md:pt-0">
<div className="md:hidden">
{invoices?.map((invoice) => (
<div
key={invoice.id}
className="mb-2 w-full rounded-md bg-white p-4"
>
<div className="flex items-center justify-between border-b pb-4">
<div>
<div className="mb-2 flex items-center">
<Image
src={invoice.image_url}
className="mr-2 rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`}
/>
<p>{invoice.name}</p>
</div>
<p className="text-sm text-gray-500">{invoice.email}</p>
</div>
<InvoiceStatus status={invoice.status} />
</div>
<div className="flex w-full items-center justify-between pt-4">
<div>
<p className="text-xl font-medium">
{formatCurrency(invoice.amount)}
</p>
<p>{formatDateToLocal(invoice.date)}</p>
</div>
<div className="flex justify-end gap-2">
<UpdateInvoice id={invoice.id} />
<DeleteInvoice id={invoice.id} />
</div>
</div>
</div>
))}
</div>
<table className="hidden min-w-full text-gray-900 md:table">
<thead className="rounded-lg text-left text-sm font-normal">
<tr>
<th scope="col" className="px-4 py-5 font-medium sm:pl-6">
Customer
</th>
<th scope="col" className="px-3 py-5 font-medium">
Email
</th>
<th scope="col" className="px-3 py-5 font-medium">
Amount
</th>
<th scope="col" className="px-3 py-5 font-medium">
Date
</th>
<th scope="col" className="px-3 py-5 font-medium">
Status
</th>
<th scope="col" className="relative py-3 pl-6 pr-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white">
{invoices?.map((invoice) => (
<tr
key={invoice.id}
className="w-full border-b py-3 text-sm last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg"
>
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex items-center gap-3">
<Image
src={invoice.image_url}
className="rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`}
/>
<p>{invoice.name}</p>
</div>
</td>
<td className="whitespace-nowrap px-3 py-3">
{invoice.email}
</td>
<td className="whitespace-nowrap px-3 py-3">
{formatCurrency(invoice.amount)}
</td>
<td className="whitespace-nowrap px-3 py-3">
{formatDateToLocal(invoice.date)}
</td>
<td className="whitespace-nowrap px-3 py-3">
<InvoiceStatus status={invoice.status} />
</td>
<td className="whitespace-nowrap py-3 pl-6 pr-3">
<div className="flex justify-end gap-3">
<UpdateInvoice id={invoice.id} />
<DeleteInvoice id={invoice.id} />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,75 @@
'use client';
import { lusitana } from '@/app/ui/fonts';
import {
AtSymbolIcon,
KeyIcon,
ExclamationCircleIcon,
} from '@heroicons/react/24/outline';
import { ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from './button';
export default function LoginForm() {
return (
<form className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
Please log in to continue.
</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="email"
>
Email
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="email"
type="email"
name="email"
placeholder="Enter your email address"
required
/>
<AtSymbolIcon 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>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password"
required
minLength={6}
/>
<KeyIcon 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>
</div>
</div>
<LoginButton />
<div className="flex h-8 items-end space-x-1">
{/* Add form errors here */}
</div>
</div>
</form>
);
}
function LoginButton() {
return (
<Button className="mt-4 w-full">
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
}

View File

@@ -0,0 +1,43 @@
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
export default function Search({ placeholder }: { placeholder: string }) {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
params.set('page', '1');
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
<MagnifyingGlassIcon className="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

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

1680
dashboard/starter-example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"@heroicons/react": "^2.0.18",
"@tailwindcss/forms": "^0.5.6",
"@types/node": "20.5.7",
"@vercel/postgres": "^0.5.0",
"autoprefixer": "10.4.15",
"clsx": "^2.0.0",
"next": "^14.0.0",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"zod": "^3.22.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.1",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.14",
"dotenv": "^16.3.1",
"prettier": "^3.0.3"
},
"engines": {
"node": ">=18"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -0,0 +1,168 @@
const { sql } = require('@vercel/postgres');
const {
invoices,
customers,
revenue,
users,
} = require('../app/lib/placeholder-data.js');
const bcrypt = require('bcrypt');
async function seedUsers() {
try {
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// Create the "invoices" table if it doesn't exist
const createTable = await sql`
CREATE TABLE IF NOT EXISTS users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);
`;
console.log(`Created "users" table`);
// Insert data into the "users" table
const insertedUsers = await Promise.all(
users.map(async (user) => {
const hashedPassword = await bcrypt.hash(user.password, 10);
return sql`
INSERT INTO users (id, name, email, password)
VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword})
ON CONFLICT (id) DO NOTHING;
`;
}),
);
console.log(`Seeded ${insertedUsers.length} users`);
return {
createTable,
users: insertedUsers,
};
} catch (error) {
console.error('Error seeding users:', error);
throw error;
}
}
async function seedInvoices() {
try {
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// Create the "invoices" table if it doesn't exist
const createTable = await sql`
CREATE TABLE IF NOT EXISTS invoices (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
customer_id UUID NOT NULL,
amount INT NOT NULL,
status VARCHAR(255) NOT NULL,
date DATE NOT NULL
);
`;
console.log(`Created "invoices" table`);
// Insert data into the "invoices" table
const insertedInvoices = await Promise.all(
invoices.map(
(invoice) => sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${invoice.customer_id}, ${invoice.amount}, ${invoice.status}, ${invoice.date})
ON CONFLICT (id) DO NOTHING;
`,
),
);
console.log(`Seeded ${insertedInvoices.length} invoices`);
return {
createTable,
invoices: insertedInvoices,
};
} catch (error) {
console.error('Error seeding invoices:', error);
throw error;
}
}
async function seedCustomers() {
try {
await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// Create the "customers" table if it doesn't exist
const createTable = await sql`
CREATE TABLE IF NOT EXISTS customers (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
image_url VARCHAR(255) NOT NULL
);
`;
console.log(`Created "customers" table`);
// Insert data into the "customers" table
const insertedCustomers = await Promise.all(
customers.map(
(customer) => sql`
INSERT INTO customers (id, name, email, image_url)
VALUES (${customer.id}, ${customer.name}, ${customer.email}, ${customer.image_url})
ON CONFLICT (id) DO NOTHING;
`,
),
);
console.log(`Seeded ${insertedCustomers.length} customers`);
return {
createTable,
customers: insertedCustomers,
};
} catch (error) {
console.error('Error seeding customers:', error);
throw error;
}
}
async function seedRevenue() {
try {
// Create the "revenue" table if it doesn't exist
const createTable = await sql`
CREATE TABLE IF NOT EXISTS revenue (
month VARCHAR(4) NOT NULL UNIQUE,
revenue INT NOT NULL
);
`;
console.log(`Created "revenue" table`);
// Insert data into the "revenue" table
const insertedRevenue = await Promise.all(
revenue.map(
(rev) => sql`
INSERT INTO revenue (month, revenue)
VALUES (${rev.month}, ${rev.revenue})
ON CONFLICT (month) DO NOTHING;
`,
),
);
console.log(`Seeded ${insertedRevenue.length} revenue`);
return {
createTable,
revenue: insertedRevenue,
};
} catch (error) {
console.error('Error seeding revenue:', error);
throw error;
}
}
(async () => {
await seedUsers();
await seedCustomers();
await seedInvoices();
await seedRevenue();
})();

View File

@@ -0,0 +1,33 @@
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
gridTemplateColumns: {
'13': 'repeat(13, minmax(0, 1fr))',
},
colors: {
blue: {
400: '#2589FE',
500: '#0070F3',
600: '#2F6FEB',
},
},
},
keyframes: {
shimmer: {
'100%': {
transform: 'translateX(100%)',
},
},
},
},
plugins: [require('@tailwindcss/forms')],
};
export default config;

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"app/lib/placeholder-data.js",
"scripts/seed.js"
],
"exclude": ["node_modules"]
}

5753
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

331
pnpm-lock.yaml generated
View File

@@ -10,13 +10,13 @@ importers:
devDependencies:
'@vercel/style-guide':
specifier: ^5.0.1
version: 5.0.1(eslint@8.48.0)(prettier@3.0.3)(typescript@4.9.5)
version: 5.0.1(eslint@8.48.0)(prettier@3.0.3)(typescript@5.2.2)
eslint:
specifier: 8.48.0
version: 8.48.0
eslint-config-next:
specifier: 13.4.19
version: 13.4.19(eslint@8.48.0)(typescript@4.9.5)
version: 13.4.19(eslint@8.48.0)(typescript@5.2.2)
eslint-config-prettier:
specifier: 9.0.0
version: 9.0.0(eslint@8.48.0)
@@ -37,7 +37,7 @@ importers:
version: 4.0.3
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -55,7 +55,7 @@ importers:
dependencies:
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -73,7 +73,7 @@ importers:
version: 4.0.3
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -91,7 +91,7 @@ importers:
dependencies:
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -109,7 +109,7 @@ importers:
version: 4.0.3
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -130,7 +130,7 @@ importers:
version: 4.0.3
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -145,7 +145,7 @@ importers:
version: 4.0.3
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -157,7 +157,7 @@ importers:
dependencies:
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -169,7 +169,7 @@ importers:
dependencies:
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -278,6 +278,64 @@ importers:
specifier: ^3.0.3
version: 3.0.3
dashboard/starter-example:
dependencies:
'@heroicons/react':
specifier: ^2.0.18
version: 2.0.18(react@18.2.0)
'@tailwindcss/forms':
specifier: ^0.5.6
version: 0.5.6(tailwindcss@3.3.3)
'@types/node':
specifier: 20.5.7
version: 20.5.7
'@vercel/postgres':
specifier: ^0.5.0
version: 0.5.0
autoprefixer:
specifier: 10.4.15
version: 10.4.15(postcss@8.4.31)
clsx:
specifier: ^2.0.0
version: 2.0.0
next:
specifier: 13.5.7-canary.27
version: 13.5.7-canary.27(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
postcss:
specifier: 8.4.31
version: 8.4.31
react:
specifier: 18.2.0
version: 18.2.0
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
tailwindcss:
specifier: 3.3.3
version: 3.3.3
typescript:
specifier: 5.2.2
version: 5.2.2
zod:
specifier: ^3.22.2
version: 3.22.2
devDependencies:
'@types/bcrypt':
specifier: ^5.0.1
version: 5.0.1
'@types/react':
specifier: 18.2.21
version: 18.2.21
'@types/react-dom':
specifier: 18.2.14
version: 18.2.14
dotenv:
specifier: ^16.3.1
version: 16.3.1
prettier:
specifier: ^3.0.3
version: 3.0.3
seo/demo:
dependencies:
fuse.js:
@@ -288,7 +346,7 @@ importers:
version: 4.17.21
next:
specifier: latest
version: 13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
version: 14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -658,14 +716,14 @@ packages:
resolution: {integrity: sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==}
dev: false
/@next/env@13.5.6:
resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==}
dev: false
/@next/env@13.5.7-canary.27:
resolution: {integrity: sha512-RSvTXOOqnBr6u51WEW5VXVvXjooBjXUcW7ZCn6xltXeYP86FZP3zYKUJCbDN/ApDrTMgNwvJpp4cJcTxKGc3Kw==}
dev: false
/@next/env@14.0.0:
resolution: {integrity: sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA==}
dev: false
/@next/eslint-plugin-next@13.4.19:
resolution: {integrity: sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==}
dependencies:
@@ -681,8 +739,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-arm64@13.5.6:
resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}
/@next/swc-darwin-arm64@13.5.7-canary.27:
resolution: {integrity: sha512-4ups4pgSufJk4oMgMGm9LeFeq3uJOL5Gu3Lzd4RQkoHwLO6NdM0jX5bNuQTr4YNS5sNcdIuQGXTraKAWkGhP9Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -690,8 +748,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-arm64@13.5.7-canary.27:
resolution: {integrity: sha512-4ups4pgSufJk4oMgMGm9LeFeq3uJOL5Gu3Lzd4RQkoHwLO6NdM0jX5bNuQTr4YNS5sNcdIuQGXTraKAWkGhP9Q==}
/@next/swc-darwin-arm64@14.0.0:
resolution: {integrity: sha512-HQKi159jCz4SRsPesVCiNN6tPSAFUkOuSkpJsqYTIlbHLKr1mD6be/J0TvWV6fwJekj81bZV9V/Tgx3C2HO9lA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -708,8 +766,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@13.5.6:
resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==}
/@next/swc-darwin-x64@13.5.7-canary.27:
resolution: {integrity: sha512-R4PO3dhiQ8n1iSCa+kZALLcgh5KenwVk5IaEJNkoKEhyrQ4sz4fertt+22Z6JJlzfFJQUKqZ6lJFBcMuSzIAHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -717,8 +775,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@13.5.7-canary.27:
resolution: {integrity: sha512-R4PO3dhiQ8n1iSCa+kZALLcgh5KenwVk5IaEJNkoKEhyrQ4sz4fertt+22Z6JJlzfFJQUKqZ6lJFBcMuSzIAHQ==}
/@next/swc-darwin-x64@14.0.0:
resolution: {integrity: sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -735,8 +793,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.5.6:
resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==}
/@next/swc-linux-arm64-gnu@13.5.7-canary.27:
resolution: {integrity: sha512-k2O5AcG0jGEihS60BRKjx/ZIGe9GakmXibxFZcMx5Uoz9RuUPRpYNw5NlxW+vPmjm4GPEZwL2BykrOqeiYhvcw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -744,8 +802,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.5.7-canary.27:
resolution: {integrity: sha512-k2O5AcG0jGEihS60BRKjx/ZIGe9GakmXibxFZcMx5Uoz9RuUPRpYNw5NlxW+vPmjm4GPEZwL2BykrOqeiYhvcw==}
/@next/swc-linux-arm64-gnu@14.0.0:
resolution: {integrity: sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -762,8 +820,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.5.6:
resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==}
/@next/swc-linux-arm64-musl@13.5.7-canary.27:
resolution: {integrity: sha512-7kN/tDn9J1exdKOLbCAS93oBJbU4Mhk0lSjBtGqK5G1Enz82ncpY1T2v2Vhr690ihwsLw1UAM24AlwZ0qQnBaQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -771,8 +829,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.5.7-canary.27:
resolution: {integrity: sha512-7kN/tDn9J1exdKOLbCAS93oBJbU4Mhk0lSjBtGqK5G1Enz82ncpY1T2v2Vhr690ihwsLw1UAM24AlwZ0qQnBaQ==}
/@next/swc-linux-arm64-musl@14.0.0:
resolution: {integrity: sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -789,8 +847,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.5.6:
resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==}
/@next/swc-linux-x64-gnu@13.5.7-canary.27:
resolution: {integrity: sha512-qSh27B6Fv/QmQe4/yxLKICGdV1A3B5KNSwYyAy8loC/7bK+dB/w37vO//23iOndBgQJ/u1rC6B1gmMipqVV4rA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -798,8 +856,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.5.7-canary.27:
resolution: {integrity: sha512-qSh27B6Fv/QmQe4/yxLKICGdV1A3B5KNSwYyAy8loC/7bK+dB/w37vO//23iOndBgQJ/u1rC6B1gmMipqVV4rA==}
/@next/swc-linux-x64-gnu@14.0.0:
resolution: {integrity: sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -816,8 +874,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@13.5.6:
resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==}
/@next/swc-linux-x64-musl@13.5.7-canary.27:
resolution: {integrity: sha512-ZpkAyXVe2xqupPfVCj9KzE4FZygLUTzvi3IHdR+wBt41pcEfihz5rhgtoE37WkAu7TkDJTdjW3Lbp39re4NDSw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -825,8 +883,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@13.5.7-canary.27:
resolution: {integrity: sha512-ZpkAyXVe2xqupPfVCj9KzE4FZygLUTzvi3IHdR+wBt41pcEfihz5rhgtoE37WkAu7TkDJTdjW3Lbp39re4NDSw==}
/@next/swc-linux-x64-musl@14.0.0:
resolution: {integrity: sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -843,8 +901,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.5.6:
resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==}
/@next/swc-win32-arm64-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-wPGQ17Hs0XIytgFkFm385J43m3TUEK/Gvr+kqJUPja4/JIha1llCKR11W1whPHon4YQ+GhX8J8QmL09rulStsg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -852,8 +910,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-wPGQ17Hs0XIytgFkFm385J43m3TUEK/Gvr+kqJUPja4/JIha1llCKR11W1whPHon4YQ+GhX8J8QmL09rulStsg==}
/@next/swc-win32-arm64-msvc@14.0.0:
resolution: {integrity: sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -870,8 +928,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.5.6:
resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==}
/@next/swc-win32-ia32-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-SrEymZnyK70LUCJ8TdSma2T5jxjlo2RP35w1glG0a2vFWAQ4s23/1z7kRRNEO4GvQHHXQtD2y3uEL3RcZY12Sw==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -879,8 +937,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-SrEymZnyK70LUCJ8TdSma2T5jxjlo2RP35w1glG0a2vFWAQ4s23/1z7kRRNEO4GvQHHXQtD2y3uEL3RcZY12Sw==}
/@next/swc-win32-ia32-msvc@14.0.0:
resolution: {integrity: sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -897,8 +955,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.5.6:
resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==}
/@next/swc-win32-x64-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-vbdekFDbPmKuIhN6elPnE5srlsaNiF4j0txuEPlDGF6yn4dd4Tpiko3G1pJgqqrFwftjhtwommAmwJ17nrGnIA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -906,8 +964,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.5.7-canary.27:
resolution: {integrity: sha512-vbdekFDbPmKuIhN6elPnE5srlsaNiF4j0txuEPlDGF6yn4dd4Tpiko3G1pJgqqrFwftjhtwommAmwJ17nrGnIA==}
/@next/swc-win32-x64-msvc@14.0.0:
resolution: {integrity: sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1063,7 +1121,7 @@ packages:
resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==}
dev: false
/@typescript-eslint/eslint-plugin@6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@4.9.5):
/@typescript-eslint/eslint-plugin@6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -1075,10 +1133,10 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.9.0
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
'@typescript-eslint/scope-manager': 6.7.3
'@typescript-eslint/type-utils': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/utils': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/type-utils': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
'@typescript-eslint/utils': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
'@typescript-eslint/visitor-keys': 6.7.3
debug: 4.3.4
eslint: 8.48.0
@@ -1086,13 +1144,13 @@ packages:
ignore: 5.2.4
natural-compare: 1.4.0
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@4.9.5)
typescript: 4.9.5
ts-api-utils: 1.0.3(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@6.7.3(eslint@8.48.0)(typescript@4.9.5):
/@typescript-eslint/parser@6.7.3(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -1104,11 +1162,11 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 6.7.3
'@typescript-eslint/types': 6.7.3
'@typescript-eslint/typescript-estree': 6.7.3(typescript@4.9.5)
'@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2)
'@typescript-eslint/visitor-keys': 6.7.3
debug: 4.3.4
eslint: 8.48.0
typescript: 4.9.5
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
@@ -1129,7 +1187,7 @@ packages:
'@typescript-eslint/visitor-keys': 6.7.3
dev: true
/@typescript-eslint/type-utils@6.7.3(eslint@8.48.0)(typescript@4.9.5):
/@typescript-eslint/type-utils@6.7.3(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -1139,12 +1197,12 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 6.7.3(typescript@4.9.5)
'@typescript-eslint/utils': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2)
'@typescript-eslint/utils': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
debug: 4.3.4
eslint: 8.48.0
ts-api-utils: 1.0.3(typescript@4.9.5)
typescript: 4.9.5
ts-api-utils: 1.0.3(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
@@ -1159,7 +1217,7 @@ packages:
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5):
/@typescript-eslint/typescript-estree@5.62.0(typescript@5.2.2):
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -1174,13 +1232,13 @@ packages:
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
tsutils: 3.21.0(typescript@4.9.5)
typescript: 4.9.5
tsutils: 3.21.0(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@6.7.3(typescript@4.9.5):
/@typescript-eslint/typescript-estree@6.7.3(typescript@5.2.2):
resolution: {integrity: sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -1195,13 +1253,13 @@ packages:
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@4.9.5)
typescript: 4.9.5
ts-api-utils: 1.0.3(typescript@5.2.2)
typescript: 5.2.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@5.62.0(eslint@8.48.0)(typescript@4.9.5):
/@typescript-eslint/utils@5.62.0(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@@ -1212,7 +1270,7 @@ packages:
'@types/semver': 7.5.3
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5)
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2)
eslint: 8.48.0
eslint-scope: 5.1.1
semver: 7.5.4
@@ -1221,7 +1279,7 @@ packages:
- typescript
dev: true
/@typescript-eslint/utils@6.7.3(eslint@8.48.0)(typescript@4.9.5):
/@typescript-eslint/utils@6.7.3(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -1232,7 +1290,7 @@ packages:
'@types/semver': 7.5.3
'@typescript-eslint/scope-manager': 6.7.3
'@typescript-eslint/types': 6.7.3
'@typescript-eslint/typescript-estree': 6.7.3(typescript@4.9.5)
'@typescript-eslint/typescript-estree': 6.7.3(typescript@5.2.2)
eslint: 8.48.0
semver: 7.5.4
transitivePeerDependencies:
@@ -1266,7 +1324,7 @@ packages:
ws: 8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)
dev: false
/@vercel/style-guide@5.0.1(eslint@8.48.0)(prettier@3.0.3)(typescript@4.9.5):
/@vercel/style-guide@5.0.1(eslint@8.48.0)(prettier@3.0.3)(typescript@5.2.2):
resolution: {integrity: sha512-3J/5xpwJ2Wk+cKB3EGY2KCdVQycaThLKhjBmgXPfIKb+E74lPpXVIDfaQE0D2JoAyIzGsqdH7Lbmr+DojwofxQ==}
engines: {node: '>=16'}
peerDependencies:
@@ -1287,25 +1345,25 @@ packages:
'@babel/core': 7.23.0
'@babel/eslint-parser': 7.22.15(@babel/core@7.23.0)(eslint@8.48.0)
'@rushstack/eslint-patch': 1.5.0
'@typescript-eslint/eslint-plugin': 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/eslint-plugin': 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@5.2.2)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
eslint: 8.48.0
eslint-config-prettier: 9.0.0(eslint@8.48.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.28.1)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.48.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.48.0)
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-typescript@3.6.1)(eslint@8.48.0)
eslint-plugin-jest: 27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@4.9.5)
eslint-plugin-jest: 27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@5.2.2)
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.48.0)
eslint-plugin-playwright: 0.16.0(eslint-plugin-jest@27.4.0)(eslint@8.48.0)
eslint-plugin-react: 7.33.2(eslint@8.48.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.48.0)
eslint-plugin-testing-library: 6.0.2(eslint@8.48.0)(typescript@4.9.5)
eslint-plugin-testing-library: 6.0.2(eslint@8.48.0)(typescript@5.2.2)
eslint-plugin-tsdoc: 0.2.17
eslint-plugin-unicorn: 48.0.1(eslint@8.48.0)
prettier: 3.0.3
prettier-plugin-packagejson: 2.4.6(prettier@3.0.3)
typescript: 4.9.5
typescript: 5.2.2
transitivePeerDependencies:
- eslint-import-resolver-node
- eslint-import-resolver-webpack
@@ -2062,7 +2120,7 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-config-next@13.4.19(eslint@8.48.0)(typescript@4.9.5):
/eslint-config-next@13.4.19(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0
@@ -2073,7 +2131,7 @@ packages:
dependencies:
'@next/eslint-plugin-next': 13.4.19
'@rushstack/eslint-patch': 1.5.0
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
eslint: 8.48.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.3)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.48.0)
@@ -2081,7 +2139,7 @@ packages:
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.48.0)
eslint-plugin-react: 7.33.2(eslint@8.48.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.48.0)
typescript: 4.9.5
typescript: 5.2.2
transitivePeerDependencies:
- eslint-import-resolver-webpack
- supports-color
@@ -2159,7 +2217,7 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
debug: 3.2.7
eslint: 8.48.0
eslint-import-resolver-node: 0.3.9
@@ -2189,7 +2247,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/parser': 6.7.3(eslint@8.48.0)(typescript@5.2.2)
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
@@ -2214,7 +2272,7 @@ packages:
- supports-color
dev: true
/eslint-plugin-jest@27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@4.9.5):
/eslint-plugin-jest@27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-ukVeKmMPAUA5SWjHenvyyXnirKfHKMdOsTZdn5tZx5EW05HGVQwBohigjFZGGj3zuv1cV6hc82FvWv6LdIbkgg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
@@ -2227,8 +2285,8 @@ packages:
jest:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/eslint-plugin': 6.7.3(@typescript-eslint/parser@6.7.3)(eslint@8.48.0)(typescript@5.2.2)
'@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@5.2.2)
eslint: 8.48.0
transitivePeerDependencies:
- supports-color
@@ -2270,7 +2328,7 @@ packages:
optional: true
dependencies:
eslint: 8.48.0
eslint-plugin-jest: 27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@4.9.5)
eslint-plugin-jest: 27.4.0(@typescript-eslint/eslint-plugin@6.7.3)(eslint@8.48.0)(typescript@5.2.2)
dev: true
/eslint-plugin-react-hooks@4.6.0(eslint@8.48.0):
@@ -2307,13 +2365,13 @@ packages:
string.prototype.matchall: 4.0.10
dev: true
/eslint-plugin-testing-library@6.0.2(eslint@8.48.0)(typescript@4.9.5):
/eslint-plugin-testing-library@6.0.2(eslint@8.48.0)(typescript@5.2.2):
resolution: {integrity: sha512-3BV6FWtLbpKFb4Y1obSdt8PC9xSqz6T+7EHB/6pSCXqVjKPoS67ck903feKMKQphd5VhrX+N51yHuVaPa7elsw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'}
peerDependencies:
eslint: ^7.5.0 || ^8.0.0
dependencies:
'@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@4.9.5)
'@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@5.2.2)
eslint: 8.48.0
transitivePeerDependencies:
- supports-color
@@ -3864,45 +3922,6 @@ packages:
- babel-plugin-macros
dev: false
/next@13.5.6(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==}
engines: {node: '>=16.14.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
sass:
optional: true
dependencies:
'@next/env': 13.5.6
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001541
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(@babel/core@7.23.0)(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 13.5.6
'@next/swc-darwin-x64': 13.5.6
'@next/swc-linux-arm64-gnu': 13.5.6
'@next/swc-linux-arm64-musl': 13.5.6
'@next/swc-linux-x64-gnu': 13.5.6
'@next/swc-linux-x64-musl': 13.5.6
'@next/swc-win32-arm64-msvc': 13.5.6
'@next/swc-win32-ia32-msvc': 13.5.6
'@next/swc-win32-x64-msvc': 13.5.6
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: false
/next@13.5.7-canary.27(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-a80Cp8kw6+FWxOEHAVqr4/uoQ4bD/E/JJmDm7+gQfl5oW4g84NnyKVt8B2hqwk8prjDnc/tQ88Ot+E6vZeVWsg==}
engines: {node: '>=18.17.0'}
@@ -3942,6 +3961,45 @@ packages:
- babel-plugin-macros
dev: false
/next@14.0.0(@babel/core@7.23.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
sass:
optional: true
dependencies:
'@next/env': 14.0.0
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001541
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(@babel/core@7.23.0)(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 14.0.0
'@next/swc-darwin-x64': 14.0.0
'@next/swc-linux-arm64-gnu': 14.0.0
'@next/swc-linux-arm64-musl': 14.0.0
'@next/swc-linux-x64-gnu': 14.0.0
'@next/swc-linux-x64-musl': 14.0.0
'@next/swc-win32-arm64-msvc': 14.0.0
'@next/swc-win32-ia32-msvc': 14.0.0
'@next/swc-win32-x64-msvc': 14.0.0
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: false
/node-addon-api@5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
dev: false
@@ -5143,13 +5201,13 @@ packages:
resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==}
dev: false
/ts-api-utils@1.0.3(typescript@4.9.5):
/ts-api-utils@1.0.3(typescript@5.2.2):
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
engines: {node: '>=16.13.0'}
peerDependencies:
typescript: '>=4.2.0'
dependencies:
typescript: 4.9.5
typescript: 5.2.2
dev: true
/ts-interface-checker@0.1.13:
@@ -5172,14 +5230,14 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
/tsutils@3.21.0(typescript@4.9.5):
/tsutils@3.21.0(typescript@5.2.2):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
typescript: 4.9.5
typescript: 5.2.2
dev: true
/type-check@0.4.0:
@@ -5252,7 +5310,6 @@ packages:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
hasBin: true
dev: false
/unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}

View File

@@ -1,4 +0,0 @@
packages:
- 'basics/**/*'
- 'dashboard/**/*'
- 'seo/*'