mirror of
https://github.com/michivonah/nextjs.git
synced 2025-12-22 14:06:29 +01:00
setup db connection + implement data fetching on dashboard + add skeleton + docs
This commit is contained in:
parent
aea677e6f7
commit
00c39d3416
11 changed files with 259 additions and 67 deletions
61
README.md
61
README.md
|
|
@ -79,6 +79,19 @@ The main function of the app or just a file is defined by `export default`.
|
|||
|
||||
The layout of the app is defined in `layout.js`. The content of `layout.js` is shared between all pages.
|
||||
|
||||
## Imports
|
||||
When importing function and components from other files, there can be named & default imports. Default imports are when you import the default function of a file which was defined by `export default function`.
|
||||
|
||||
Default import
|
||||
```tsx
|
||||
import LatestInvoices from '../ui/dashboard/latest-invoices';
|
||||
```
|
||||
|
||||
Named import
|
||||
```tsx
|
||||
import { fetchRevenue } from '../lib/data';
|
||||
```
|
||||
|
||||
## Styling
|
||||
In `global.css` some global CSS styling rules were defined. Usally this file is imported in the root layout of the app `layout.js`/`layout.jsx`/`layout.tsx`.
|
||||
|
||||
|
|
@ -316,6 +329,54 @@ export default function NavLinks() {
|
|||
}
|
||||
```
|
||||
|
||||
## Data rendering behaviors
|
||||
There are two ways of fetching data in Next.js.
|
||||
- static rendering
|
||||
- dynamic rendering
|
||||
|
||||
Static rendering means that data is fetch while building the app or when the data is revalidating. On each access the cached data will be served. This has the advantage, that the server load can be reduced and the app is faster. Also it has advantages in point of SEO, because there has nothing to be loaded from the server except the page itself.
|
||||
|
||||
But this approach isn't useful when personalized data should be shown. There comes dynamic rendering into play.
|
||||
Dynamic rendering enables to render real time data and personalized content for each user. It's also possible to show infos about the request.
|
||||
|
||||
### Data streaming
|
||||
Data streaming enables you to transfer data to the client in more smaller chunks instead of one full package. So the users see the content, that is already ready and in the background the rest is loaded. So the app feels faster and the user can begin using it earlier. Also the data can be rendered in parellel, instead of waiting until each request is finished.
|
||||
|
||||
By adding a `loading.tsx`-File with a `Loading()` component to the app, the streaming can be enabled.
|
||||
```tsx
|
||||
export default function Loading(){
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
The component `Loading()` is shown as a fallback while the "real" content is loaded.
|
||||
|
||||
Instead of ugly text a skeleton can be implemented. This has the advantage that the user knows where he can expect content and layout shift is minimized.
|
||||
|
||||
This loading behaviour is applied to all subpages of the folder its defined in. To prevent this the files, on which it should be appended, can be moved inside a folder in parenthesis. Folders with parenthesis allows to group pages logical, without affecting the URL path.
|
||||
|
||||
With `Suspense` its also possible to just add a skeleton to one component. Then the data fetching process has to be a part of the component. To use `Suspense` the component has to be wrapped inside it.
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
|
||||
|
||||
export default async function Page(){
|
||||
return (
|
||||
<main>
|
||||
<div className='mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8'>
|
||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||
<RevenueChart />
|
||||
</Suspense>
|
||||
<LatestInvoices latestInvoices={latestInvoices} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
When multiple components should load at the same time, they should be moved in a own component and then be put into `Suspense`.
|
||||
|
||||
## Ressources
|
||||
- [Next.js Installation](https://nextjs.org/docs/app/getting-started/installation)
|
||||
- [Next.js React Foundations Course](https://nextjs.org/learn/react-foundations)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Copy from .env.local on the Vercel dashboard
|
||||
# https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
|
||||
POSTGRES_URL=
|
||||
POSTGRES_PRISMA_URL=
|
||||
POSTGRES_URL_NON_POOLING=
|
||||
POSTGRES_USER=
|
||||
POSTGRES_HOST=
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_DATABASE=
|
||||
|
||||
# `openssl rand -base64 32`
|
||||
AUTH_SECRET=
|
||||
AUTH_URL=http://localhost:3000/api/auth
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import DashboardSkeleton from "../../ui/skeletons";
|
||||
|
||||
export default function Loading(){
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
30
dashboard-app-course/app/dashboard/(overview)/page.tsx
Normal file
30
dashboard-app-course/app/dashboard/(overview)/page.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Card } from '@/app/ui/dashboard/cards'
|
||||
import RevenueChart from '../../ui/dashboard/revenue-chart';
|
||||
import LatestInvoices from '../../ui/dashboard/latest-invoices';
|
||||
import { lusitana } from '../../ui/fonts';
|
||||
import { Suspense } from 'react';
|
||||
import { RevenueChartSkeleton, LatestInvoicesSkeleton, CardsSkeleton, CardSkeleton} from '@/app/ui/skeletons';
|
||||
import CardWrapper from '@/app/ui/dashboard/cards';
|
||||
|
||||
export default async function Page(){
|
||||
return (
|
||||
<main>
|
||||
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2x1`}>
|
||||
Dashboard
|
||||
</h1>
|
||||
<div className='grid gap-6 sm: grid-cols-2 lg:grid-cols-4'>
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<CardWrapper />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className='mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8'>
|
||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||
<RevenueChart />
|
||||
</Suspense>
|
||||
<Suspense fallback={<LatestInvoicesSkeleton />}>
|
||||
<LatestInvoices />
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function Page(){
|
||||
return <p>Dashboard page</p>;
|
||||
}
|
||||
|
|
@ -1,26 +1,22 @@
|
|||
// import postgres from 'postgres';
|
||||
import postgres from 'postgres';
|
||||
|
||||
// const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
||||
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
||||
|
||||
// async function listInvoices() {
|
||||
// const data = await sql`
|
||||
// SELECT invoices.amount, customers.name
|
||||
// FROM invoices
|
||||
// JOIN customers ON invoices.customer_id = customers.id
|
||||
// WHERE invoices.amount = 666;
|
||||
// `;
|
||||
async function listInvoices() {
|
||||
const data = await sql`
|
||||
SELECT invoices.amount, customers.name
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE invoices.amount = 666;
|
||||
`;
|
||||
|
||||
// return data;
|
||||
// }
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({
|
||||
message:
|
||||
'Uncomment this file and remove this line. You can delete this file when you are finished.',
|
||||
});
|
||||
// try {
|
||||
// return Response.json(await listInvoices());
|
||||
// } catch (error) {
|
||||
// return Response.json({ error }, { status: 500 });
|
||||
// }
|
||||
try {
|
||||
return Response.json(await listInvoices());
|
||||
} catch (error) {
|
||||
return Response.json({ error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
InboxIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { lusitana } from '@/app/ui/fonts';
|
||||
import { fetchCardData } from '@/app/lib/data';
|
||||
|
||||
const iconMap = {
|
||||
collected: BanknotesIcon,
|
||||
|
|
@ -14,18 +15,23 @@ const iconMap = {
|
|||
};
|
||||
|
||||
export default async function CardWrapper() {
|
||||
const {
|
||||
numberOfInvoices,
|
||||
numberOfCustomers,
|
||||
totalPaidInvoices,
|
||||
totalPendingInvoices,
|
||||
} = await fetchCardData();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* NOTE: Uncomment this code in Chapter 9 */}
|
||||
|
||||
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" />
|
||||
<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"
|
||||
/> */}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,18 @@ import clsx from 'clsx';
|
|||
import Image from 'next/image';
|
||||
import { lusitana } from '@/app/ui/fonts';
|
||||
import { LatestInvoice } from '@/app/lib/definitions';
|
||||
export default async function LatestInvoices({
|
||||
latestInvoices,
|
||||
}: {
|
||||
latestInvoices: LatestInvoice[];
|
||||
}) {
|
||||
import { fetchLatestInvoices } from '@/app/lib/data';
|
||||
|
||||
export default async function LatestInvoices() {
|
||||
const latestInvoices = await fetchLatestInvoices();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col md: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">
|
||||
{/* NOTE: Uncomment this code in Chapter 7 */}
|
||||
|
||||
{/* <div className="bg-white px-6">
|
||||
<div className="bg-white px-6">
|
||||
{latestInvoices.map((invoice, i) => {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -53,7 +51,7 @@ export default async function LatestInvoices({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { generateYAxis } from '@/app/lib/utils';
|
|||
import { CalendarIcon } from '@heroicons/react/24/outline';
|
||||
import { lusitana } from '@/app/ui/fonts';
|
||||
import { Revenue } from '@/app/lib/definitions';
|
||||
import { fetchRevenue } from '@/app/lib/data';
|
||||
|
||||
// This component is representational only.
|
||||
// For data visualization UI, check out:
|
||||
|
|
@ -9,28 +10,22 @@ import { Revenue } from '@/app/lib/definitions';
|
|||
// https://www.chartjs.org/
|
||||
// https://airbnb.io/visx/
|
||||
|
||||
export default async function RevenueChart({
|
||||
revenue,
|
||||
}: {
|
||||
revenue: Revenue[];
|
||||
}) {
|
||||
export default async function RevenueChart() {
|
||||
const revenue = await fetchRevenue();
|
||||
|
||||
const chartHeight = 350;
|
||||
// NOTE: Uncomment this code in Chapter 7
|
||||
const { yAxisLabels, topLabel } = generateYAxis(revenue);
|
||||
|
||||
// const { yAxisLabels, topLabel } = generateYAxis(revenue);
|
||||
|
||||
// if (!revenue || revenue.length === 0) {
|
||||
// return <p className="mt-4 text-gray-400">No data available.</p>;
|
||||
// }
|
||||
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>
|
||||
{/* NOTE: Uncomment this code in Chapter 7 */}
|
||||
|
||||
{/* <div className="rounded-xl bg-gray-50 p-4">
|
||||
<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">
|
||||
<div
|
||||
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
|
||||
|
|
@ -59,7 +54,7 @@ export default async function RevenueChart({
|
|||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
122
dashboard-app-course/package-lock.json
generated
122
dashboard-app-course/package-lock.json
generated
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "nextjs-dashboard",
|
||||
"name": "dashboard-app-course",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@neondatabase/serverless": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"autoprefixer": "10.4.20",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
|
@ -602,6 +603,19 @@
|
|||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@neondatabase/serverless": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.0.0.tgz",
|
||||
"integrity": "sha512-XWmEeWpBXIoksZSDN74kftfTnXFEGZ3iX8jbANWBc+ag6dsiQuvuR4LgB0WdCOKMb5AQgjqgufc0TgAsZubUYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz",
|
||||
|
|
@ -837,12 +851,22 @@
|
|||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.13.tgz",
|
||||
"integrity": "sha512-6kXByGkvRvwXLuyaWzsebs2du6+XuAB2CuMsuzP7uaihQahshVgSmB22Pmh0vQMkQ1h5+PZU0d+Di1o+WpVWJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
"pg-types": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.7.tgz",
|
||||
|
|
@ -2099,6 +2123,12 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/obuf": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -2154,6 +2184,48 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-numeric": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
|
||||
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz",
|
||||
"integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
|
||||
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"pg-numeric": "1.0.2",
|
||||
"postgres-array": "~3.0.1",
|
||||
"postgres-bytea": "~3.0.0",
|
||||
"postgres-date": "~2.1.0",
|
||||
"postgres-interval": "^3.0.0",
|
||||
"postgres-range": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -2311,6 +2383,51 @@
|
|||
"url": "https://github.com/sponsors/porsager"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
|
||||
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
|
||||
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"obuf": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
|
||||
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
|
||||
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-range": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
|
||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.11.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
||||
|
|
@ -2953,7 +3070,6 @@
|
|||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@neondatabase/serverless": "^1.0.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"autoprefixer": "10.4.20",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue