diff --git a/README.md b/README.md index 1c2f5cc..32d1909 100644 --- a/README.md +++ b/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
Loading...
; +} +``` + +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 ( +
+
+ }> + + + +
+
+ ); +} +``` + +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) diff --git a/dashboard-app-course/.env.example b/dashboard-app-course/.env.example deleted file mode 100644 index 8a85ba7..0000000 --- a/dashboard-app-course/.env.example +++ /dev/null @@ -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 \ No newline at end of file diff --git a/dashboard-app-course/app/dashboard/(overview)/loading.tsx b/dashboard-app-course/app/dashboard/(overview)/loading.tsx new file mode 100644 index 0000000..e8d4f4f --- /dev/null +++ b/dashboard-app-course/app/dashboard/(overview)/loading.tsx @@ -0,0 +1,5 @@ +import DashboardSkeleton from "../../ui/skeletons"; + +export default function Loading(){ + return ; +} \ No newline at end of file diff --git a/dashboard-app-course/app/dashboard/(overview)/page.tsx b/dashboard-app-course/app/dashboard/(overview)/page.tsx new file mode 100644 index 0000000..dbf7731 --- /dev/null +++ b/dashboard-app-course/app/dashboard/(overview)/page.tsx @@ -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 ( +
+

+ Dashboard +

+
+ }> + + +
+
+ }> + + + }> + + +
+
+ ); +} \ No newline at end of file diff --git a/dashboard-app-course/app/dashboard/page.tsx b/dashboard-app-course/app/dashboard/page.tsx deleted file mode 100644 index 4f5f9e4..0000000 --- a/dashboard-app-course/app/dashboard/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page(){ - return

Dashboard page

; -} \ No newline at end of file diff --git a/dashboard-app-course/app/query/route.ts b/dashboard-app-course/app/query/route.ts index 0701b73..adc27f8 100644 --- a/dashboard-app-course/app/query/route.ts +++ b/dashboard-app-course/app/query/route.ts @@ -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 }); + } } diff --git a/dashboard-app-course/app/ui/dashboard/cards.tsx b/dashboard-app-course/app/ui/dashboard/cards.tsx index 526e6f9..dfb5aa2 100644 --- a/dashboard-app-course/app/ui/dashboard/cards.tsx +++ b/dashboard-app-course/app/ui/dashboard/cards.tsx @@ -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 */} - - {/* + */} + /> ); } diff --git a/dashboard-app-course/app/ui/dashboard/latest-invoices.tsx b/dashboard-app-course/app/ui/dashboard/latest-invoices.tsx index 27b74f6..8b1695a 100644 --- a/dashboard-app-course/app/ui/dashboard/latest-invoices.tsx +++ b/dashboard-app-course/app/ui/dashboard/latest-invoices.tsx @@ -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 (

Latest Invoices

- {/* NOTE: Uncomment this code in Chapter 7 */} - - {/*
+
{latestInvoices.map((invoice, i) => { return (
); })} -
*/} +

Updated just now

diff --git a/dashboard-app-course/app/ui/dashboard/revenue-chart.tsx b/dashboard-app-course/app/ui/dashboard/revenue-chart.tsx index f19e698..01b851d 100644 --- a/dashboard-app-course/app/ui/dashboard/revenue-chart.tsx +++ b/dashboard-app-course/app/ui/dashboard/revenue-chart.tsx @@ -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

No data available.

; - // } + if (!revenue || revenue.length === 0) { + return

No data available.

; + } return (

Recent Revenue

- {/* NOTE: Uncomment this code in Chapter 7 */} - - {/*
+

Last 12 months

-
*/} +
); } diff --git a/dashboard-app-course/package-lock.json b/dashboard-app-course/package-lock.json index 460a238..03ca9cf 100644 --- a/dashboard-app-course/package-lock.json +++ b/dashboard-app-course/package-lock.json @@ -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": { diff --git a/dashboard-app-course/package.json b/dashboard-app-course/package.json index 3d69178..a09f92a 100644 --- a/dashboard-app-course/package.json +++ b/dashboard-app-course/package.json @@ -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",