The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .eslintrc.js
├── .github
    ├── dependabot.yml
    └── workflows
    │   └── test.yml
├── .gitignore
├── .prettierignore
├── README.md
├── basics
    ├── README.md
    ├── api-routes-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── date.js
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── lib
    │   │   └── posts.js
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── [id].js
    │   ├── posts
    │   │   ├── pre-rendering.md
    │   │   └── ssg-ssr.md
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── assets-metadata-css-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── package.json
    │   ├── pages
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── first-post.js
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── vercel.svg
    │   └── styles
    │   │   ├── Home.module.css
    │   │   └── global.css
    ├── basics-final
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── date.js
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── lib
    │   │   └── posts.js
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── api
    │   │   │   └── hello.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── [id].js
    │   ├── posts
    │   │   ├── pre-rendering.md
    │   │   └── ssg-ssr.md
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── data-fetching-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── first-post.js
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── demo
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── date.js
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── lib
    │   │   └── posts.js
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── api
    │   │   │   └── hello.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── [id].js
    │   ├── posts
    │   │   ├── pre-rendering.md
    │   │   └── ssg-ssr.md
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── dynamic-routes-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── lib
    │   │   └── posts.js
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── first-post.js
    │   ├── posts
    │   │   ├── pre-rendering.md
    │   │   └── ssg-ssr.md
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── dynamic-routes-step-1
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │   │   ├── layout.js
    │   │   └── layout.module.css
    │   ├── lib
    │   │   └── posts.js
    │   ├── package.json
    │   ├── pages
    │   │   ├── _app.js
    │   │   ├── index.js
    │   │   └── posts
    │   │   │   └── [id].js
    │   ├── posts
    │   │   ├── pre-rendering.md
    │   │   └── ssg-ssr.md
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── images
    │   │   │   └── profile.jpg
    │   └── styles
    │   │   ├── global.css
    │   │   └── utils.module.css
    ├── errors
    │   └── install.md
    ├── learn-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── package.json
    │   ├── pages
    │   │   └── index.js
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── vercel.svg
    │   └── styles
    │   │   ├── Home.module.css
    │   │   └── global.css
    ├── navigate-between-pages-starter
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── package.json
    │   ├── pages
    │   │   └── index.js
    │   ├── public
    │   │   ├── favicon.ico
    │   │   └── vercel.svg
    │   └── styles
    │   │   ├── Home.module.css
    │   │   └── global.css
    ├── snippets
    │   └── link-classname-example.js
    └── typescript-final
    │   ├── .gitignore
    │   ├── .nvmrc
    │   ├── README.md
    │   ├── components
    │       ├── date.tsx
    │       ├── layout.module.css
    │       └── layout.tsx
    │   ├── global.d.ts
    │   ├── lib
    │       └── posts.ts
    │   ├── next-env.d.ts
    │   ├── package.json
    │   ├── pages
    │       ├── _app.tsx
    │       ├── api
    │       │   └── hello.ts
    │       ├── index.tsx
    │       └── posts
    │       │   └── [id].tsx
    │   ├── posts
    │       ├── pre-rendering.md
    │       └── ssg-ssr.md
    │   ├── public
    │       ├── favicon.ico
    │       └── images
    │       │   └── profile.jpg
    │   ├── styles
    │       ├── global.css
    │       └── utils.module.css
    │   └── tsconfig.json
├── dashboard
    ├── README.md
    ├── final-example
    │   ├── .env.example
    │   ├── .gitignore
    │   ├── README.md
    │   ├── app
    │   │   ├── dashboard
    │   │   │   ├── (overview)
    │   │   │   │   ├── loading.tsx
    │   │   │   │   └── page.tsx
    │   │   │   ├── customers
    │   │   │   │   └── page.tsx
    │   │   │   ├── invoices
    │   │   │   │   ├── [id]
    │   │   │   │   │   └── edit
    │   │   │   │   │   │   ├── not-found.tsx
    │   │   │   │   │   │   └── page.tsx
    │   │   │   │   ├── create
    │   │   │   │   │   └── page.tsx
    │   │   │   │   ├── error.tsx
    │   │   │   │   └── page.tsx
    │   │   │   └── layout.tsx
    │   │   ├── favicon.ico
    │   │   ├── layout.tsx
    │   │   ├── lib
    │   │   │   ├── actions.ts
    │   │   │   ├── data.ts
    │   │   │   ├── definitions.ts
    │   │   │   ├── placeholder-data.ts
    │   │   │   └── utils.ts
    │   │   ├── login
    │   │   │   └── page.tsx
    │   │   ├── opengraph-image.png
    │   │   ├── page.tsx
    │   │   ├── query
    │   │   │   └── route.ts
    │   │   ├── seed
    │   │   │   └── route.ts
    │   │   └── ui
    │   │   │   ├── acme-logo.tsx
    │   │   │   ├── button.tsx
    │   │   │   ├── customers
    │   │   │       └── table.tsx
    │   │   │   ├── dashboard
    │   │   │       ├── cards.tsx
    │   │   │       ├── latest-invoices.tsx
    │   │   │       ├── nav-links.tsx
    │   │   │       ├── revenue-chart.tsx
    │   │   │       └── sidenav.tsx
    │   │   │   ├── fonts.ts
    │   │   │   ├── global.css
    │   │   │   ├── invoices
    │   │   │       ├── breadcrumbs.tsx
    │   │   │       ├── buttons.tsx
    │   │   │       ├── create-form.tsx
    │   │   │       ├── edit-form.tsx
    │   │   │       ├── pagination.tsx
    │   │   │       ├── status.tsx
    │   │   │       └── table.tsx
    │   │   │   ├── login-form.tsx
    │   │   │   ├── search.tsx
    │   │   │   └── skeletons.tsx
    │   ├── auth.config.ts
    │   ├── auth.ts
    │   ├── middleware.ts
    │   ├── next.config.ts
    │   ├── package.json
    │   ├── pnpm-lock.yaml
    │   ├── postcss.config.js
    │   ├── public
    │   │   ├── customers
    │   │   │   ├── amy-burns.png
    │   │   │   ├── balazs-orban.png
    │   │   │   ├── delba-de-oliveira.png
    │   │   │   ├── evil-rabbit.png
    │   │   │   ├── lee-robinson.png
    │   │   │   └── michael-novotny.png
    │   │   ├── hero-desktop.png
    │   │   └── hero-mobile.png
    │   ├── tailwind.config.ts
    │   └── tsconfig.json
    └── starter-example
    │   ├── .env.example
    │   ├── .gitignore
    │   ├── README.md
    │   ├── app
    │       ├── layout.tsx
    │       ├── lib
    │       │   ├── data.ts
    │       │   ├── definitions.ts
    │       │   ├── placeholder-data.ts
    │       │   └── utils.ts
    │       ├── page.tsx
    │       ├── query
    │       │   └── route.ts
    │       ├── seed
    │       │   └── route.ts
    │       └── ui
    │       │   ├── acme-logo.tsx
    │       │   ├── button.tsx
    │       │   ├── customers
    │       │       └── table.tsx
    │       │   ├── dashboard
    │       │       ├── cards.tsx
    │       │       ├── latest-invoices.tsx
    │       │       ├── nav-links.tsx
    │       │       ├── revenue-chart.tsx
    │       │       └── sidenav.tsx
    │       │   ├── global.css
    │       │   ├── invoices
    │       │       ├── breadcrumbs.tsx
    │       │       ├── buttons.tsx
    │       │       ├── create-form.tsx
    │       │       ├── edit-form.tsx
    │       │       ├── pagination.tsx
    │       │       ├── status.tsx
    │       │       └── table.tsx
    │       │   ├── login-form.tsx
    │       │   ├── search.tsx
    │       │   └── skeletons.tsx
    │   ├── next.config.ts
    │   ├── package.json
    │   ├── pnpm-lock.yaml
    │   ├── postcss.config.js
    │   ├── public
    │       ├── customers
    │       │   ├── amy-burns.png
    │       │   ├── balazs-orban.png
    │       │   ├── delba-de-oliveira.png
    │       │   ├── evil-rabbit.png
    │       │   ├── lee-robinson.png
    │       │   └── michael-novotny.png
    │       ├── favicon.ico
    │       ├── hero-desktop.png
    │       ├── hero-mobile.png
    │       └── opengraph-image.png
    │   ├── tailwind.config.ts
    │   └── tsconfig.json
├── license.md
├── package.json
├── pnpm-lock.yaml
├── prettier.config.js
└── seo
    ├── .gitignore
    ├── README.md
    ├── components
        └── CodeSampleModal.js
    ├── countries.js
    ├── demo
        ├── .gitignore
        ├── README.md
        ├── components
        │   └── CodeSampleModal.js
        ├── countries.js
        ├── package.json
        ├── pages
        │   ├── _app.js
        │   └── index.js
        ├── public
        │   ├── favicon.ico
        │   ├── large-image.jpg
        │   └── vercel.svg
        └── styles
        │   ├── Home.module.css
        │   └── global.css
    ├── package.json
    ├── pages
        ├── _app.js
        └── index.js
    ├── public
        ├── favicon.ico
        ├── large-image.jpg
        └── vercel.svg
    └── styles
        ├── Home.module.css
        └── global.css


/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   extends: ['next/core-web-vitals', 'prettier'],
 3 |   ignorePatterns: ['**/.next/**', '**/node_modules/**'],
 4 |   root: true,
 5 |   settings: {
 6 |     next: {
 7 |       rootDir: ['basics/*/', 'dashboard/*/', 'seo/'],
 8 |     },
 9 |   },
10 | };
11 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |   - package-ecosystem: 'github-actions'
4 |     directory: '/'
5 |     schedule:
6 |       interval: 'weekly'
7 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
 1 | name: test
 2 | on: pull_request
 3 | jobs:
 4 |   test:
 5 |     runs-on: ubuntu-latest
 6 |     steps:
 7 |       - name: Cancel running workflows
 8 |         uses: styfle/cancel-workflow-action@0.12.1
 9 |         with:
10 |           access_token: ${{ github.token }}
11 |       - name: Checkout repo
12 |         uses: actions/checkout@v4
13 |       - name: Setup pnpm
14 |         uses: pnpm/action-setup@v3
15 |       - name: Set node version
16 |         uses: actions/setup-node@v3
17 |         with:
18 |           cache: 'pnpm'
19 |           node-version: '20'
20 |       - name: Cache node_modules
21 |         id: node-modules-cache
22 |         uses: actions/cache@v4
23 |         with:
24 |           path: '**/node_modules'
25 |           key: node-modules-cache-${{ hashFiles('**/pnpm-lock.yaml') }}
26 |       - name: Install dependencies
27 |         if: steps.node-modules-cache.outputs.cache-hit != 'true'
28 |         run: pnpm install
29 |       - name: Run tests
30 |         run: pnpm test
31 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | 


--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/.next
2 | **/node_modules
3 | **/package-lock.json
4 | **/pnpm-lock.yaml
5 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Learn Next.js
 2 | 
 3 | This repository contains starter templates and final code for [Learn Next.js](https://nextjs.org/learn) courses:
 4 | 
 5 | - 🆕 [Learn Next.js App Router, Data Fetching, Databases, and Auth](https://nextjs.org/learn) ([demo](https://next-learn-dashboard.vercel.sh))
 6 | - [Learn Basics and TypeScript](https://nextjs.org/learn-pages-router/basics/create-nextjs-app) ([demo](https://next-learn-starter.vercel.app))
 7 | - [Learn SEO](https://nextjs.org/learn-pages-router/seo/introduction-to-seo) ([demo](https://next-seo-starter.vercel.app))
 8 | 
 9 | ## Contributions
10 | 
11 | The code for the example apps you build using Next.js Learn live in this repository and we'd be grateful for your contributions.
12 | 
13 | The course curriculum is currently not open sourced, but you can [create an issue](https://github.com/vercel/next-learn/issues/new) if you find a mistake.
14 | 


--------------------------------------------------------------------------------
/basics/README.md:
--------------------------------------------------------------------------------
1 | # next-learn-starter
2 | 
3 | This repository contains starter templates for [Learn Next.js](https://nextjs.org/learn).
4 | 
5 | The final result for the basics lesson can be found in the [demo](demo) directory and is available at: [https://next-learn-starter.vercel.app/](https://next-learn-starter.vercel.app/).
6 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/components/date.js:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns';
2 | 
3 | export default function Date({ dateString }) {
4 |   const date = parseISO(dateString);
5 |   return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
6 | }
7 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = '[Your Name]';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({ children, home }) {
11 |   return (
12 |     <div className={styles.container}>
13 |       <Head>
14 |         <link rel="icon" href="/favicon.ico" />
15 |         <meta
16 |           name="description"
17 |           content="Learn how to build a personal website using Next.js"
18 |         />
19 |         <meta
20 |           property="og:image"
21 |           content={`https://og-image.vercel.app/${encodeURI(
22 |             siteTitle,
23 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
24 |         />
25 |         <meta name="og:title" content={siteTitle} />
26 |         <meta name="twitter:card" content="summary_large_image" />
27 |       </Head>
28 |       <header className={styles.header}>
29 |         {home ? (
30 |           <>
31 |             <Image
32 |               priority
33 |               src="/images/profile.jpg"
34 |               className={utilStyles.borderCircle}
35 |               height={144}
36 |               width={144}
37 |               alt={name}
38 |             />
39 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
40 |           </>
41 |         ) : (
42 |           <>
43 |             <Link href="/">
44 |               <Image
45 |                 priority
46 |                 src="/images/profile.jpg"
47 |                 className={utilStyles.borderCircle}
48 |                 height={108}
49 |                 width={108}
50 |                 alt={name}
51 |               />
52 |             </Link>
53 |             <h2 className={utilStyles.headingLg}>
54 |               <Link href="/" className={utilStyles.colorInherit}>
55 |                 {name}
56 |               </Link>
57 |             </h2>
58 |           </>
59 |         )}
60 |       </header>
61 |       <main>{children}</main>
62 |       {!home && (
63 |         <div className={styles.backToHome}>
64 |           <Link href="/">← Back to home</Link>
65 |         </div>
66 |       )}
67 |     </div>
68 |   );
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/lib/posts.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | import { remark } from 'remark';
 5 | import html from 'remark-html';
 6 | 
 7 | const postsDirectory = path.join(process.cwd(), 'posts');
 8 | 
 9 | export function getSortedPostsData() {
10 |   // Get file names under /posts
11 |   const fileNames = fs.readdirSync(postsDirectory);
12 |   const allPostsData = fileNames.map((fileName) => {
13 |     // Remove ".md" from file name to get id
14 |     const id = fileName.replace(/\.md$/, '');
15 | 
16 |     // Read markdown file as string
17 |     const fullPath = path.join(postsDirectory, fileName);
18 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
19 | 
20 |     // Use gray-matter to parse the post metadata section
21 |     const matterResult = matter(fileContents);
22 | 
23 |     // Combine the data with the id
24 |     return {
25 |       id,
26 |       ...matterResult.data,
27 |     };
28 |   });
29 |   // Sort posts by date
30 |   return allPostsData.sort((a, b) => {
31 |     if (a.date < b.date) {
32 |       return 1;
33 |     } else {
34 |       return -1;
35 |     }
36 |   });
37 | }
38 | 
39 | export function getAllPostIds() {
40 |   const fileNames = fs.readdirSync(postsDirectory);
41 |   return fileNames.map((fileName) => {
42 |     return {
43 |       params: {
44 |         id: fileName.replace(/\.md$/, ''),
45 |       },
46 |     };
47 |   });
48 | }
49 | 
50 | export async function getPostData(id) {
51 |   const fullPath = path.join(postsDirectory, `${id}.md`);
52 |   const fileContents = fs.readFileSync(fullPath, 'utf8');
53 | 
54 |   // Use gray-matter to parse the post metadata section
55 |   const matterResult = matter(fileContents);
56 | 
57 |   // Use remark to convert markdown into HTML string
58 |   const processedContent = await remark()
59 |     .use(html)
60 |     .process(matterResult.content);
61 |   const contentHtml = processedContent.toString();
62 | 
63 |   // Combine the data with the id and contentHtml
64 |   return {
65 |     id,
66 |     contentHtml,
67 |     ...matterResult.data,
68 |   };
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "date-fns": "^2.29.3",
10 |     "gray-matter": "^4.0.3",
11 |     "next": "latest",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "remark": "^14.0.2",
15 |     "remark-html": "^15.0.1"
16 |   },
17 |   "engines": {
18 |     "node": ">=18"
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | import Link from 'next/link';
 6 | import Date from '../components/date';
 7 | 
 8 | export default function Home({ allPostsData }) {
 9 |   return (
10 |     <Layout home>
11 |       <Head>
12 |         <title>{siteTitle}</title>
13 |       </Head>
14 |       <section className={utilStyles.headingMd}>
15 |         <p>[Your Self Introduction]</p>
16 |         <p>
17 |           (This is a sample website - you’ll be building a site like this in{' '}
18 |           <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
19 |         </p>
20 |       </section>
21 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
22 |         <h2 className={utilStyles.headingLg}>Blog</h2>
23 |         <ul className={utilStyles.list}>
24 |           {allPostsData.map(({ id, date, title }) => (
25 |             <li className={utilStyles.listItem} key={id}>
26 |               <Link href={`/posts/${id}`}>{title}</Link>
27 |               <br />
28 |               <small className={utilStyles.lightText}>
29 |                 <Date dateString={date} />
30 |               </small>
31 |             </li>
32 |           ))}
33 |         </ul>
34 |       </section>
35 |     </Layout>
36 |   );
37 | }
38 | 
39 | export async function getStaticProps() {
40 |   const allPostsData = getSortedPostsData();
41 |   return {
42 |     props: {
43 |       allPostsData,
44 |     },
45 |   };
46 | }
47 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/pages/posts/[id].js:
--------------------------------------------------------------------------------
 1 | import Layout from '../../components/layout';
 2 | import { getAllPostIds, getPostData } from '../../lib/posts';
 3 | import Head from 'next/head';
 4 | import Date from '../../components/date';
 5 | import utilStyles from '../../styles/utils.module.css';
 6 | 
 7 | export default function Post({ postData }) {
 8 |   return (
 9 |     <Layout>
10 |       <Head>
11 |         <title>{postData.title}</title>
12 |       </Head>
13 |       <article>
14 |         <h1 className={utilStyles.headingXl}>{postData.title}</h1>
15 |         <div className={utilStyles.lightText}>
16 |           <Date dateString={postData.date} />
17 |         </div>
18 |         <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
19 |       </article>
20 |     </Layout>
21 |   );
22 | }
23 | 
24 | export async function getStaticPaths() {
25 |   const paths = getAllPostIds();
26 |   return {
27 |     paths,
28 |     fallback: false,
29 |   };
30 | }
31 | 
32 | export async function getStaticProps({ params }) {
33 |   const postData = await getPostData(params.id);
34 |   return {
35 |     props: {
36 |       postData,
37 |     },
38 |   };
39 | }
40 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/api-routes-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/api-routes-starter/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/api-routes-starter/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/api-routes-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/api-routes-starter/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "next": "latest",
10 |     "react": "latest",
11 |     "react-dom": "latest"
12 |   },
13 |   "engines": {
14 |     "node": ">=18"
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/pages/posts/first-post.js:
--------------------------------------------------------------------------------
 1 | import Link from 'next/link';
 2 | 
 3 | export default function FirstPost() {
 4 |   return (
 5 |     <>
 6 |       <h1>First Post</h1>
 7 |       <h2>
 8 |         <Link href="/">Back to home</Link>
 9 |       </h2>
10 |     </>
11 |   );
12 | }
13 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/assets-metadata-css-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/public/vercel.svg:
--------------------------------------------------------------------------------
1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2 |     xmlns="http://www.w3.org/2000/svg">
3 |     <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4 | </svg>


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/styles/Home.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   min-height: 100vh;
 3 |   padding: 0 0.5rem;
 4 |   display: flex;
 5 |   flex-direction: column;
 6 |   justify-content: center;
 7 |   align-items: center;
 8 | }
 9 | 
10 | .title a {
11 |   color: #0070f3;
12 |   text-decoration: none;
13 | }
14 | 
15 | .title a:hover,
16 | .title a:focus,
17 | .title a:active {
18 |   text-decoration: underline;
19 | }
20 | 
21 | .title {
22 |   margin: 0 0 1rem;
23 |   line-height: 1.15;
24 |   font-size: 3.6rem;
25 | }
26 | 
27 | .title {
28 |   text-align: center;
29 | }
30 | 
31 | .title,
32 | .description {
33 |   text-align: center;
34 | }
35 | 
36 | .description {
37 |   line-height: 1.5;
38 |   font-size: 1.5rem;
39 | }
40 | 
41 | .grid {
42 |   display: flex;
43 |   align-items: center;
44 |   justify-content: center;
45 |   flex-wrap: wrap;
46 | 
47 |   max-width: 800px;
48 |   margin-top: 3rem;
49 | }
50 | 
51 | .card {
52 |   margin: 1rem;
53 |   flex-basis: 45%;
54 |   padding: 1.5rem;
55 |   text-align: left;
56 |   color: inherit;
57 |   text-decoration: none;
58 |   border: 1px solid #eaeaea;
59 |   border-radius: 10px;
60 |   transition:
61 |     color 0.15s ease,
62 |     border-color 0.15s ease;
63 | }
64 | 
65 | .card:hover,
66 | .card:focus,
67 | .card:active {
68 |   color: #0070f3;
69 |   border-color: #0070f3;
70 | }
71 | 
72 | .card h3 {
73 |   margin: 0 0 1rem 0;
74 |   font-size: 1.5rem;
75 | }
76 | 
77 | .card p {
78 |   margin: 0;
79 |   font-size: 1.25rem;
80 |   line-height: 1.5;
81 | }
82 | 
83 | .logo {
84 |   height: 1em;
85 | }
86 | 
87 | @media (max-width: 600px) {
88 |   .grid {
89 |     width: 100%;
90 |     flex-direction: column;
91 |   }
92 | }
93 | 


--------------------------------------------------------------------------------
/basics/assets-metadata-css-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     Inter,
 7 |     -apple-system,
 8 |     BlinkMacSystemFont,
 9 |     Segoe UI,
10 |     Roboto,
11 |     Oxygen,
12 |     Ubuntu,
13 |     Cantarell,
14 |     Fira Sans,
15 |     Droid Sans,
16 |     Helvetica Neue,
17 |     sans-serif;
18 | }
19 | 
20 | a {
21 |   color: inherit;
22 |   text-decoration: none;
23 | }
24 | 
25 | * {
26 |   box-sizing: border-box;
27 | }
28 | 
29 | img {
30 |   max-width: 100%;
31 |   height: auto;
32 | }
33 | 


--------------------------------------------------------------------------------
/basics/basics-final/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/basics-final/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/basics-final/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/basics-final/components/date.js:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns';
2 | 
3 | export default function Date({ dateString }) {
4 |   const date = parseISO(dateString);
5 |   return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
6 | }
7 | 


--------------------------------------------------------------------------------
/basics/basics-final/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import Script from 'next/script';
 4 | 
 5 | import styles from './layout.module.css';
 6 | import utilStyles from '../styles/utils.module.css';
 7 | import Link from 'next/link';
 8 | 
 9 | const name = '[Your Name]';
10 | export const siteTitle = 'Next.js Sample Website';
11 | 
12 | export default function Layout({ children, home }) {
13 |   return (
14 |     <div className={styles.container}>
15 |       <Head>
16 |         <link rel="icon" href="/favicon.ico" />
17 |         <meta
18 |           name="description"
19 |           content="Learn how to build a personal website using Next.js"
20 |         />
21 |         <meta
22 |           property="og:image"
23 |           content={`https://og-image.vercel.app/${encodeURI(
24 |             siteTitle,
25 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
26 |         />
27 |         <meta name="og:title" content={siteTitle} />
28 |         <meta name="twitter:card" content="summary_large_image" />
29 |       </Head>
30 |       <Script
31 |         src="https://connect.facebook.net/en_US/sdk.js"
32 |         strategy="lazyOnload"
33 |         onLoad={() =>
34 |           console.log(`script loaded correctly, window.FB has been populated`)
35 |         }
36 |       />
37 |       <header className={styles.header}>
38 |         {home ? (
39 |           <>
40 |             <Image
41 |               priority
42 |               src="/images/profile.jpg"
43 |               className={utilStyles.borderCircle}
44 |               height={144}
45 |               width={144}
46 |               alt={name}
47 |             />
48 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
49 |           </>
50 |         ) : (
51 |           <>
52 |             <Link href="/">
53 |               <Image
54 |                 priority
55 |                 src="/images/profile.jpg"
56 |                 className={utilStyles.borderCircle}
57 |                 height={108}
58 |                 width={108}
59 |                 alt={name}
60 |               />
61 |             </Link>
62 |             <h2 className={utilStyles.headingLg}>
63 |               <Link href="/" className={utilStyles.colorInherit}>
64 |                 {name}
65 |               </Link>
66 |             </h2>
67 |           </>
68 |         )}
69 |       </header>
70 |       <main>{children}</main>
71 |       {!home && (
72 |         <div className={styles.backToHome}>
73 |           <Link href="/">← Back to home</Link>
74 |         </div>
75 |       )}
76 |     </div>
77 |   );
78 | }
79 | 


--------------------------------------------------------------------------------
/basics/basics-final/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/basics-final/lib/posts.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | import { remark } from 'remark';
 5 | import html from 'remark-html';
 6 | 
 7 | const postsDirectory = path.join(process.cwd(), 'posts');
 8 | 
 9 | export function getSortedPostsData() {
10 |   // Get file names under /posts
11 |   const fileNames = fs.readdirSync(postsDirectory);
12 |   const allPostsData = fileNames.map((fileName) => {
13 |     // Remove ".md" from file name to get id
14 |     const id = fileName.replace(/\.md$/, '');
15 | 
16 |     // Read markdown file as string
17 |     const fullPath = path.join(postsDirectory, fileName);
18 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
19 | 
20 |     // Use gray-matter to parse the post metadata section
21 |     const matterResult = matter(fileContents);
22 | 
23 |     // Combine the data with the id
24 |     return {
25 |       id,
26 |       ...matterResult.data,
27 |     };
28 |   });
29 |   // Sort posts by date
30 |   return allPostsData.sort((a, b) => {
31 |     if (a.date < b.date) {
32 |       return 1;
33 |     } else {
34 |       return -1;
35 |     }
36 |   });
37 | }
38 | 
39 | export function getAllPostIds() {
40 |   const fileNames = fs.readdirSync(postsDirectory);
41 |   return fileNames.map((fileName) => {
42 |     return {
43 |       params: {
44 |         id: fileName.replace(/\.md$/, ''),
45 |       },
46 |     };
47 |   });
48 | }
49 | 
50 | export async function getPostData(id) {
51 |   const fullPath = path.join(postsDirectory, `${id}.md`);
52 |   const fileContents = fs.readFileSync(fullPath, 'utf8');
53 | 
54 |   // Use gray-matter to parse the post metadata section
55 |   const matterResult = matter(fileContents);
56 | 
57 |   // Use remark to convert markdown into HTML string
58 |   const processedContent = await remark()
59 |     .use(html)
60 |     .process(matterResult.content);
61 |   const contentHtml = processedContent.toString();
62 | 
63 |   // Combine the data with the id and contentHtml
64 |   return {
65 |     id,
66 |     contentHtml,
67 |     ...matterResult.data,
68 |   };
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/basics-final/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "date-fns": "^2.29.3",
10 |     "gray-matter": "^4.0.3",
11 |     "next": "latest",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "remark": "^14.0.2",
15 |     "remark-html": "^15.0.1"
16 |   },
17 |   "engines": {
18 |     "node": ">=18"
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/basics/basics-final/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/basics-final/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | export default (req, res) => {
2 |   res.status(200).json({ text: 'Hello' });
3 | };
4 | 


--------------------------------------------------------------------------------
/basics/basics-final/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | import Link from 'next/link';
 6 | import Date from '../components/date';
 7 | 
 8 | export default function Home({ allPostsData }) {
 9 |   return (
10 |     <Layout home>
11 |       <Head>
12 |         <title>{siteTitle}</title>
13 |       </Head>
14 |       <section className={utilStyles.headingMd}>
15 |         <p>[Your Self Introduction]</p>
16 |         <p>
17 |           (This is a sample website - you’ll be building a site like this in{' '}
18 |           <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
19 |         </p>
20 |       </section>
21 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
22 |         <h2 className={utilStyles.headingLg}>Blog</h2>
23 |         <ul className={utilStyles.list}>
24 |           {allPostsData.map(({ id, date, title }) => (
25 |             <li className={utilStyles.listItem} key={id}>
26 |               <Link href={`/posts/${id}`}>{title}</Link>
27 |               <br />
28 |               <small className={utilStyles.lightText}>
29 |                 <Date dateString={date} />
30 |               </small>
31 |             </li>
32 |           ))}
33 |         </ul>
34 |       </section>
35 |     </Layout>
36 |   );
37 | }
38 | 
39 | export async function getStaticProps() {
40 |   const allPostsData = getSortedPostsData();
41 |   return {
42 |     props: {
43 |       allPostsData,
44 |     },
45 |   };
46 | }
47 | 


--------------------------------------------------------------------------------
/basics/basics-final/pages/posts/[id].js:
--------------------------------------------------------------------------------
 1 | import Layout from '../../components/layout';
 2 | import { getAllPostIds, getPostData } from '../../lib/posts';
 3 | import Head from 'next/head';
 4 | import Date from '../../components/date';
 5 | import utilStyles from '../../styles/utils.module.css';
 6 | 
 7 | export default function Post({ postData }) {
 8 |   return (
 9 |     <Layout>
10 |       <Head>
11 |         <title>{postData.title}</title>
12 |       </Head>
13 |       <article>
14 |         <h1 className={utilStyles.headingXl}>{postData.title}</h1>
15 |         <div className={utilStyles.lightText}>
16 |           <Date dateString={postData.date} />
17 |         </div>
18 |         <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
19 |       </article>
20 |     </Layout>
21 |   );
22 | }
23 | 
24 | export async function getStaticPaths() {
25 |   const paths = getAllPostIds();
26 |   return {
27 |     paths,
28 |     fallback: false,
29 |   };
30 | }
31 | 
32 | export async function getStaticProps({ params }) {
33 |   const postData = await getPostData(params.id);
34 |   return {
35 |     props: {
36 |       postData,
37 |     },
38 |   };
39 | }
40 | 


--------------------------------------------------------------------------------
/basics/basics-final/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/basics-final/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/basics-final/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/basics-final/public/favicon.ico


--------------------------------------------------------------------------------
/basics/basics-final/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/basics-final/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/basics-final/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/basics-final/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = '[Your Name]';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({ children, home }) {
11 |   return (
12 |     <div className={styles.container}>
13 |       <Head>
14 |         <link rel="icon" href="/favicon.ico" />
15 |         <meta
16 |           name="description"
17 |           content="Learn how to build a personal website using Next.js"
18 |         />
19 |         <meta
20 |           property="og:image"
21 |           content={`https://og-image.vercel.app/${encodeURI(
22 |             siteTitle,
23 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
24 |         />
25 |         <meta name="og:title" content={siteTitle} />
26 |         <meta name="twitter:card" content="summary_large_image" />
27 |       </Head>
28 |       <header className={styles.header}>
29 |         {home ? (
30 |           <>
31 |             <Image
32 |               priority
33 |               src="/images/profile.jpg"
34 |               className={utilStyles.borderCircle}
35 |               height={144}
36 |               width={144}
37 |               alt={name}
38 |             />
39 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
40 |           </>
41 |         ) : (
42 |           <>
43 |             <Link href="/">
44 |               <Image
45 |                 priority
46 |                 src="/images/profile.jpg"
47 |                 className={utilStyles.borderCircle}
48 |                 height={108}
49 |                 width={108}
50 |                 alt={name}
51 |               />
52 |             </Link>
53 |             <h2 className={utilStyles.headingLg}>
54 |               <Link href="/" className={utilStyles.colorInherit}>
55 |                 {name}
56 |               </Link>
57 |             </h2>
58 |           </>
59 |         )}
60 |       </header>
61 |       <main>{children}</main>
62 |       {!home && (
63 |         <div className={styles.backToHome}>
64 |           <Link href="/">← Back to home</Link>
65 |         </div>
66 |       )}
67 |     </div>
68 |   );
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "next": "latest",
10 |     "react": "latest",
11 |     "react-dom": "latest"
12 |   },
13 |   "engines": {
14 |     "node": ">=18"
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | 
 5 | export default function Home() {
 6 |   return (
 7 |     <Layout home>
 8 |       <Head>
 9 |         <title>{siteTitle}</title>
10 |       </Head>
11 |       <section className={utilStyles.headingMd}>
12 |         <p>[Your Self Introduction]</p>
13 |         <p>
14 |           (This is a sample website - you’ll be building a site like this in{' '}
15 |           <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
16 |         </p>
17 |       </section>
18 |     </Layout>
19 |   );
20 | }
21 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/pages/posts/first-post.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Link from 'next/link';
 3 | import Layout from '../../components/layout';
 4 | 
 5 | export default function FirstPost() {
 6 |   return (
 7 |     <Layout>
 8 |       <Head>
 9 |         <title>First Post</title>
10 |       </Head>
11 |       <h1>First Post</h1>
12 |       <h2>
13 |         <Link href="/">Back to home</Link>
14 |       </h2>
15 |     </Layout>
16 |   );
17 | }
18 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/data-fetching-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/data-fetching-starter/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/data-fetching-starter/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/data-fetching-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/data-fetching-starter/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/demo/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/demo/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/demo/README.md:
--------------------------------------------------------------------------------
1 | This is a final template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/demo/components/date.js:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns';
2 | 
3 | export default function Date({ dateString }) {
4 |   const date = parseISO(dateString);
5 |   return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
6 | }
7 | 


--------------------------------------------------------------------------------
/basics/demo/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = 'Shu Uesugi';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({ children, home }) {
11 |   return (
12 |     <div className={styles.container}>
13 |       <Head>
14 |         <link rel="icon" href="/favicon.ico" />
15 |         <meta
16 |           name="description"
17 |           content="Learn how to build a personal website using Next.js"
18 |         />
19 |         <meta
20 |           property="og:image"
21 |           content={`https://og-image.vercel.app/${encodeURI(
22 |             siteTitle,
23 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
24 |         />
25 |         <meta name="og:title" content={siteTitle} />
26 |         <meta name="twitter:card" content="summary_large_image" />
27 |       </Head>
28 |       <header className={styles.header}>
29 |         {home ? (
30 |           <>
31 |             <Image
32 |               priority
33 |               src="/images/profile.jpg"
34 |               className={utilStyles.borderCircle}
35 |               height={144}
36 |               width={144}
37 |               alt={name}
38 |             />
39 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
40 |           </>
41 |         ) : (
42 |           <>
43 |             <Link href="/">
44 |               <Image
45 |                 priority
46 |                 src="/images/profile.jpg"
47 |                 className={utilStyles.borderCircle}
48 |                 height={108}
49 |                 width={108}
50 |                 alt={name}
51 |               />
52 |             </Link>
53 |             <h2 className={utilStyles.headingLg}>
54 |               <Link href="/" className={utilStyles.colorInherit}>
55 |                 {name}
56 |               </Link>
57 |             </h2>
58 |           </>
59 |         )}
60 |       </header>
61 |       <main>{children}</main>
62 |       {!home && (
63 |         <div className={styles.backToHome}>
64 |           <Link href="/">← Back to home</Link>
65 |         </div>
66 |       )}
67 |     </div>
68 |   );
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/demo/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/demo/lib/posts.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | import { remark } from 'remark';
 5 | import html from 'remark-html';
 6 | 
 7 | const postsDirectory = path.join(process.cwd(), 'posts');
 8 | 
 9 | export function getSortedPostsData() {
10 |   // Get file names under /posts
11 |   const fileNames = fs.readdirSync(postsDirectory);
12 |   const allPostsData = fileNames.map((fileName) => {
13 |     // Remove ".md" from file name to get id
14 |     const id = fileName.replace(/\.md$/, '');
15 | 
16 |     // Read markdown file as string
17 |     const fullPath = path.join(postsDirectory, fileName);
18 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
19 | 
20 |     // Use gray-matter to parse the post metadata section
21 |     const matterResult = matter(fileContents);
22 | 
23 |     // Combine the data with the id
24 |     return {
25 |       id,
26 |       ...matterResult.data,
27 |     };
28 |   });
29 |   // Sort posts by date
30 |   return allPostsData.sort((a, b) => {
31 |     if (a.date < b.date) {
32 |       return 1;
33 |     } else {
34 |       return -1;
35 |     }
36 |   });
37 | }
38 | 
39 | export function getAllPostIds() {
40 |   const fileNames = fs.readdirSync(postsDirectory);
41 |   return fileNames.map((fileName) => {
42 |     return {
43 |       params: {
44 |         id: fileName.replace(/\.md$/, ''),
45 |       },
46 |     };
47 |   });
48 | }
49 | 
50 | export async function getPostData(id) {
51 |   const fullPath = path.join(postsDirectory, `${id}.md`);
52 |   const fileContents = fs.readFileSync(fullPath, 'utf8');
53 | 
54 |   // Use gray-matter to parse the post metadata section
55 |   const matterResult = matter(fileContents);
56 | 
57 |   // Use remark to convert markdown into HTML string
58 |   const processedContent = await remark()
59 |     .use(html)
60 |     .process(matterResult.content);
61 |   const contentHtml = processedContent.toString();
62 | 
63 |   // Combine the data with the id and contentHtml
64 |   return {
65 |     id,
66 |     contentHtml,
67 |     ...matterResult.data,
68 |   };
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/demo/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "date-fns": "^2.29.3",
10 |     "gray-matter": "^4.0.3",
11 |     "next": "latest",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "remark": "^14.0.2",
15 |     "remark-html": "^15.0.1"
16 |   },
17 |   "engines": {
18 |     "node": ">=18"
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/basics/demo/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/demo/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | export default (req, res) => {
2 |   res.status(200).json({ text: 'Hello' });
3 | };
4 | 


--------------------------------------------------------------------------------
/basics/demo/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | import Link from 'next/link';
 6 | import Date from '../components/date';
 7 | 
 8 | export default function Home({ allPostsData }) {
 9 |   return (
10 |     <Layout home>
11 |       <Head>
12 |         <title>{siteTitle}</title>
13 |       </Head>
14 |       <section className={utilStyles.headingMd}>
15 |         <p>
16 |           Hello, I’m <strong>Shu</strong>. I’m a software engineer and a
17 |           translator (English/Japanese). You can contact me on{' '}
18 |           <a href="https://twitter.com/chibicode">Twitter</a>.
19 |         </p>
20 |         <p>
21 |           (This is a sample website - you’ll be building a site like this in{' '}
22 |           <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
23 |         </p>
24 |       </section>
25 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
26 |         <h2 className={utilStyles.headingLg}>Blog</h2>
27 |         <ul className={utilStyles.list}>
28 |           {allPostsData.map(({ id, date, title }) => (
29 |             <li className={utilStyles.listItem} key={id}>
30 |               <Link href={`/posts/${id}`}>{title}</Link>
31 |               <br />
32 |               <small className={utilStyles.lightText}>
33 |                 <Date dateString={date} />
34 |               </small>
35 |             </li>
36 |           ))}
37 |         </ul>
38 |       </section>
39 |     </Layout>
40 |   );
41 | }
42 | 
43 | export async function getStaticProps() {
44 |   const allPostsData = getSortedPostsData();
45 |   return {
46 |     props: {
47 |       allPostsData,
48 |     },
49 |   };
50 | }
51 | 


--------------------------------------------------------------------------------
/basics/demo/pages/posts/[id].js:
--------------------------------------------------------------------------------
 1 | import Layout from '../../components/layout';
 2 | import { getAllPostIds, getPostData } from '../../lib/posts';
 3 | import Head from 'next/head';
 4 | import Date from '../../components/date';
 5 | import utilStyles from '../../styles/utils.module.css';
 6 | 
 7 | export default function Post({ postData }) {
 8 |   return (
 9 |     <Layout>
10 |       <Head>
11 |         <title>{postData.title}</title>
12 |       </Head>
13 |       <article>
14 |         <h1 className={utilStyles.headingXl}>{postData.title}</h1>
15 |         <div className={utilStyles.lightText}>
16 |           <Date dateString={postData.date} />
17 |         </div>
18 |         <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
19 |       </article>
20 |     </Layout>
21 |   );
22 | }
23 | 
24 | export async function getStaticPaths() {
25 |   const paths = getAllPostIds();
26 |   return {
27 |     paths,
28 |     fallback: false,
29 |   };
30 | }
31 | 
32 | export async function getStaticProps({ params }) {
33 |   const postData = await getPostData(params.id);
34 |   return {
35 |     props: {
36 |       postData,
37 |     },
38 |   };
39 | }
40 | 


--------------------------------------------------------------------------------
/basics/demo/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/demo/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/demo/public/favicon.ico


--------------------------------------------------------------------------------
/basics/demo/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/demo/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/demo/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/demo/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = '[Your Name]';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({ children, home }) {
11 |   return (
12 |     <div className={styles.container}>
13 |       <Head>
14 |         <link rel="icon" href="/favicon.ico" />
15 |         <meta
16 |           name="description"
17 |           content="Learn how to build a personal website using Next.js"
18 |         />
19 |         <meta
20 |           property="og:image"
21 |           content={`https://og-image.vercel.app/${encodeURI(
22 |             siteTitle,
23 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
24 |         />
25 |         <meta name="og:title" content={siteTitle} />
26 |         <meta name="twitter:card" content="summary_large_image" />
27 |       </Head>
28 |       <header className={styles.header}>
29 |         {home ? (
30 |           <>
31 |             <Image
32 |               priority
33 |               src="/images/profile.jpg"
34 |               className={utilStyles.borderCircle}
35 |               height={144}
36 |               width={144}
37 |               alt={name}
38 |             />
39 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
40 |           </>
41 |         ) : (
42 |           <>
43 |             <Link href="/">
44 |               <Image
45 |                 priority
46 |                 src="/images/profile.jpg"
47 |                 className={utilStyles.borderCircle}
48 |                 height={108}
49 |                 width={108}
50 |                 alt={name}
51 |               />
52 |             </Link>
53 |             <h2 className={utilStyles.headingLg}>
54 |               <Link href="/" className={utilStyles.colorInherit}>
55 |                 {name}
56 |               </Link>
57 |             </h2>
58 |           </>
59 |         )}
60 |       </header>
61 |       <main>{children}</main>
62 |       {!home && (
63 |         <div className={styles.backToHome}>
64 |           <Link href="/">← Back to home</Link>
65 |         </div>
66 |       )}
67 |     </div>
68 |   );
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/lib/posts.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | 
 5 | const postsDirectory = path.join(process.cwd(), 'posts');
 6 | 
 7 | export function getSortedPostsData() {
 8 |   // Get file names under /posts
 9 |   const fileNames = fs.readdirSync(postsDirectory);
10 |   const allPostsData = fileNames.map((fileName) => {
11 |     // Remove ".md" from file name to get id
12 |     const id = fileName.replace(/\.md$/, '');
13 | 
14 |     // Read markdown file as string
15 |     const fullPath = path.join(postsDirectory, fileName);
16 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
17 | 
18 |     // Use gray-matter to parse the post metadata section
19 |     const matterResult = matter(fileContents);
20 | 
21 |     // Combine the data with the id
22 |     return {
23 |       id,
24 |       ...matterResult.data,
25 |     };
26 |   });
27 |   // Sort posts by date
28 |   return allPostsData.sort((a, b) => {
29 |     if (a.date < b.date) {
30 |       return 1;
31 |     } else {
32 |       return -1;
33 |     }
34 |   });
35 | }
36 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "gray-matter": "^4.0.3",
10 |     "next": "latest",
11 |     "react": "latest",
12 |     "react-dom": "latest"
13 |   },
14 |   "engines": {
15 |     "node": ">=18"
16 |   }
17 | }
18 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | 
 6 | export default function Home({ allPostsData }) {
 7 |   return (
 8 |     <Layout home>
 9 |       <Head>
10 |         <title>{siteTitle}</title>
11 |       </Head>
12 |       <section className={utilStyles.headingMd}>
13 |         <p>[Your Self Introduction]</p>
14 |       </section>
15 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
16 |         <h2 className={utilStyles.headingLg}>Blog</h2>
17 |         <ul className={utilStyles.list}>
18 |           {allPostsData.map(({ id, date, title }) => (
19 |             <li className={utilStyles.listItem} key={id}>
20 |               {title}
21 |               <br />
22 |               {id}
23 |               <br />
24 |               {date}
25 |             </li>
26 |           ))}
27 |         </ul>
28 |       </section>
29 |     </Layout>
30 |   );
31 | }
32 | 
33 | export async function getStaticProps() {
34 |   const allPostsData = getSortedPostsData();
35 |   return {
36 |     props: {
37 |       allPostsData,
38 |     },
39 |   };
40 | }
41 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/pages/posts/first-post.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Link from 'next/link';
 3 | import Layout from '../../components/layout';
 4 | 
 5 | export default function FirstPost() {
 6 |   return (
 7 |     <Layout>
 8 |       <Head>
 9 |         <title>First Post</title>
10 |       </Head>
11 |       <h1>First Post</h1>
12 |       <h2>
13 |         <Link href="/">Back to home</Link>
14 |       </h2>
15 |     </Layout>
16 |   );
17 | }
18 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/dynamic-routes-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/dynamic-routes-starter/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-starter/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/components/layout.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = '[Your Name]';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({ children, home }) {
11 |   return (
12 |     <div className={styles.container}>
13 |       <Head>
14 |         <link rel="icon" href="/favicon.ico" />
15 |         <meta
16 |           name="description"
17 |           content="Learn how to build a personal website using Next.js"
18 |         />
19 |         <meta
20 |           property="og:image"
21 |           content={`https://og-image.vercel.app/${encodeURI(
22 |             siteTitle,
23 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
24 |         />
25 |         <meta name="og:title" content={siteTitle} />
26 |         <meta name="twitter:card" content="summary_large_image" />
27 |       </Head>
28 |       <header className={styles.header}>
29 |         {home ? (
30 |           <>
31 |             <Image
32 |               priority
33 |               src="/images/profile.jpg"
34 |               className={utilStyles.borderCircle}
35 |               height={144}
36 |               width={144}
37 |               alt={name}
38 |             />
39 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
40 |           </>
41 |         ) : (
42 |           <>
43 |             <Link href="/">
44 |               <Image
45 |                 priority
46 |                 src="/images/profile.jpg"
47 |                 className={utilStyles.borderCircle}
48 |                 height={108}
49 |                 width={108}
50 |                 alt={name}
51 |               />
52 |             </Link>
53 |             <h2 className={utilStyles.headingLg}>
54 |               <Link href="/" className={utilStyles.colorInherit}>
55 |                 {name}
56 |               </Link>
57 |             </h2>
58 |           </>
59 |         )}
60 |       </header>
61 |       <main>{children}</main>
62 |       {!home && (
63 |         <div className={styles.backToHome}>
64 |           <Link href="/">← Back to home</Link>
65 |         </div>
66 |       )}
67 |     </div>
68 |   );
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/lib/posts.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | 
 5 | const postsDirectory = path.join(process.cwd(), 'posts');
 6 | 
 7 | export function getSortedPostsData() {
 8 |   // Get file names under /posts
 9 |   const fileNames = fs.readdirSync(postsDirectory);
10 |   const allPostsData = fileNames.map((fileName) => {
11 |     // Remove ".md" from file name to get id
12 |     const id = fileName.replace(/\.md$/, '');
13 | 
14 |     // Read markdown file as string
15 |     const fullPath = path.join(postsDirectory, fileName);
16 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
17 | 
18 |     // Use gray-matter to parse the post metadata section
19 |     const matterResult = matter(fileContents);
20 | 
21 |     // Combine the data with the id
22 |     return {
23 |       id,
24 |       ...matterResult.data,
25 |     };
26 |   });
27 |   // Sort posts by date
28 |   return allPostsData.sort((a, b) => {
29 |     if (a.date < b.date) {
30 |       return 1;
31 |     } else {
32 |       return -1;
33 |     }
34 |   });
35 | }
36 | 
37 | export function getAllPostIds() {
38 |   const fileNames = fs.readdirSync(postsDirectory);
39 |   return fileNames.map((fileName) => {
40 |     return {
41 |       params: {
42 |         id: fileName.replace(/\.md$/, ''),
43 |       },
44 |     };
45 |   });
46 | }
47 | 
48 | export function getPostData(id) {
49 |   const fullPath = path.join(postsDirectory, `${id}.md`);
50 |   const fileContents = fs.readFileSync(fullPath, 'utf8');
51 | 
52 |   // Use gray-matter to parse the post metadata section
53 |   const matterResult = matter(fileContents);
54 | 
55 |   // Combine the data with the id
56 |   return {
57 |     id,
58 |     ...matterResult.data,
59 |   };
60 | }
61 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "gray-matter": "^4.0.3",
10 |     "next": "latest",
11 |     "react": "latest",
12 |     "react-dom": "latest"
13 |   },
14 |   "engines": {
15 |     "node": ">=18"
16 |   }
17 | }
18 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function App({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/pages/index.js:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | 
 6 | export default function Home({ allPostsData }) {
 7 |   return (
 8 |     <Layout home>
 9 |       <Head>
10 |         <title>{siteTitle}</title>
11 |       </Head>
12 |       <section className={utilStyles.headingMd}>
13 |         <p>[Your Self Introduction]</p>
14 |       </section>
15 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
16 |         <h2 className={utilStyles.headingLg}>Blog</h2>
17 |         <ul className={utilStyles.list}>
18 |           {allPostsData.map(({ id, date, title }) => (
19 |             <li className={utilStyles.listItem} key={id}>
20 |               {title}
21 |               <br />
22 |               {id}
23 |               <br />
24 |               {date}
25 |             </li>
26 |           ))}
27 |         </ul>
28 |       </section>
29 |     </Layout>
30 |   );
31 | }
32 | 
33 | export async function getStaticProps() {
34 |   const allPostsData = getSortedPostsData();
35 |   return {
36 |     props: {
37 |       allPostsData,
38 |     },
39 |   };
40 | }
41 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/pages/posts/[id].js:
--------------------------------------------------------------------------------
 1 | import Layout from '../../components/layout';
 2 | import { getAllPostIds, getPostData } from '../../lib/posts';
 3 | 
 4 | export default function Post({ postData }) {
 5 |   return (
 6 |     <Layout>
 7 |       {postData.title}
 8 |       <br />
 9 |       {postData.id}
10 |       <br />
11 |       {postData.date}
12 |     </Layout>
13 |   );
14 | }
15 | 
16 | export async function getStaticPaths() {
17 |   const paths = getAllPostIds();
18 |   return {
19 |     paths,
20 |     fallback: false,
21 |   };
22 | }
23 | 
24 | export async function getStaticProps({ params }) {
25 |   const postData = getPostData(params.id);
26 |   return {
27 |     props: {
28 |       postData,
29 |     },
30 |   };
31 | }
32 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/dynamic-routes-step-1/public/favicon.ico


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/dynamic-routes-step-1/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/dynamic-routes-step-1/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/errors/install.md:
--------------------------------------------------------------------------------
 1 | # Create a Next.js App - Installation Error
 2 | 
 3 | > Linked from https://nextjs.org/learn/basics/create-nextjs-app/setup
 4 | 
 5 | If you see an installation error for the following installation command:
 6 | 
 7 | ```bash
 8 | npx create-next-app nextjs-blog --example "https://github.com/vercel/next-learn/tree/main/basics/learn-starter"
 9 | ```
10 | 
11 | Try removing everything after `nextjs-blog`:
12 | 
13 | ```bash
14 | npx create-next-app nextjs-blog
15 | ```
16 | 
17 | A `Could not locate the repository` error message could be the result of your workplace or school network or proxy configuration. A temporary solution may be changing your network environment by disconnecting from your workplace or school VPN, using a VPN browser extension, or trying a different wifi connection.
18 | 
19 | If none of the steps above resolve your issue, please let us know in a [GitHub Issue](https://github.com/vercel/next-learn/issues) with the error text, your OS, and Node.js version (make sure your Node.js version 18 or higher).
20 | 


--------------------------------------------------------------------------------
/basics/learn-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | 
21 | # debug
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | 
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 | 


--------------------------------------------------------------------------------
/basics/learn-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/learn-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/learn-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "next": "latest",
10 |     "react": "latest",
11 |     "react-dom": "latest"
12 |   },
13 |   "engines": {
14 |     "node": ">=18"
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/basics/learn-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/learn-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/learn-starter/public/vercel.svg:
--------------------------------------------------------------------------------
1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2 |     xmlns="http://www.w3.org/2000/svg">
3 |     <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4 | </svg>


--------------------------------------------------------------------------------
/basics/learn-starter/styles/Home.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   min-height: 100vh;
 3 |   padding: 0 0.5rem;
 4 |   display: flex;
 5 |   flex-direction: column;
 6 |   justify-content: center;
 7 |   align-items: center;
 8 | }
 9 | 
10 | .title a {
11 |   color: #0070f3;
12 |   text-decoration: none;
13 | }
14 | 
15 | .title a:hover,
16 | .title a:focus,
17 | .title a:active {
18 |   text-decoration: underline;
19 | }
20 | 
21 | .title {
22 |   margin: 0 0 1rem;
23 |   line-height: 1.15;
24 |   font-size: 3.6rem;
25 | }
26 | 
27 | .title {
28 |   text-align: center;
29 | }
30 | 
31 | .title,
32 | .description {
33 |   text-align: center;
34 | }
35 | 
36 | .description {
37 |   line-height: 1.5;
38 |   font-size: 1.5rem;
39 | }
40 | 
41 | .grid {
42 |   display: flex;
43 |   align-items: center;
44 |   justify-content: center;
45 |   flex-wrap: wrap;
46 | 
47 |   max-width: 800px;
48 |   margin-top: 3rem;
49 | }
50 | 
51 | .card {
52 |   margin: 1rem;
53 |   flex-basis: 45%;
54 |   padding: 1.5rem;
55 |   text-align: left;
56 |   color: inherit;
57 |   text-decoration: none;
58 |   border: 1px solid #eaeaea;
59 |   border-radius: 10px;
60 |   transition:
61 |     color 0.15s ease,
62 |     border-color 0.15s ease;
63 | }
64 | 
65 | .card:hover,
66 | .card:focus,
67 | .card:active {
68 |   color: #0070f3;
69 |   border-color: #0070f3;
70 | }
71 | 
72 | .card h3 {
73 |   margin: 0 0 1rem 0;
74 |   font-size: 1.5rem;
75 | }
76 | 
77 | .card p {
78 |   margin: 0;
79 |   font-size: 1.25rem;
80 |   line-height: 1.5;
81 | }
82 | 
83 | .logo {
84 |   height: 1em;
85 | }
86 | 
87 | @media (max-width: 600px) {
88 |   .grid {
89 |     width: 100%;
90 |     flex-direction: column;
91 |   }
92 | }
93 | 


--------------------------------------------------------------------------------
/basics/learn-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     Inter,
 7 |     -apple-system,
 8 |     BlinkMacSystemFont,
 9 |     Segoe UI,
10 |     Roboto,
11 |     Oxygen,
12 |     Ubuntu,
13 |     Cantarell,
14 |     Fira Sans,
15 |     Droid Sans,
16 |     Helvetica Neue,
17 |     sans-serif;
18 | }
19 | 
20 | a {
21 |   color: inherit;
22 |   text-decoration: none;
23 | }
24 | 
25 | * {
26 |   box-sizing: border-box;
27 | }
28 | 
29 | img {
30 |   max-width: 100%;
31 |   height: auto;
32 | }
33 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "next": "latest",
10 |     "react": "latest",
11 |     "react-dom": "latest"
12 |   },
13 |   "engines": {
14 |     "node": ">=18"
15 |   }
16 | }
17 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/navigate-between-pages-starter/public/favicon.ico


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/public/vercel.svg:
--------------------------------------------------------------------------------
1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2 |     xmlns="http://www.w3.org/2000/svg">
3 |     <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4 | </svg>


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/styles/Home.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   min-height: 100vh;
 3 |   padding: 0 0.5rem;
 4 |   display: flex;
 5 |   flex-direction: column;
 6 |   justify-content: center;
 7 |   align-items: center;
 8 | }
 9 | 
10 | .title a {
11 |   color: #0070f3;
12 |   text-decoration: none;
13 | }
14 | 
15 | .title a:hover,
16 | .title a:focus,
17 | .title a:active {
18 |   text-decoration: underline;
19 | }
20 | 
21 | .title {
22 |   margin: 0 0 1rem;
23 |   line-height: 1.15;
24 |   font-size: 3.6rem;
25 | }
26 | 
27 | .title {
28 |   text-align: center;
29 | }
30 | 
31 | .title,
32 | .description {
33 |   text-align: center;
34 | }
35 | 
36 | .description {
37 |   line-height: 1.5;
38 |   font-size: 1.5rem;
39 | }
40 | 
41 | .grid {
42 |   display: flex;
43 |   align-items: center;
44 |   justify-content: center;
45 |   flex-wrap: wrap;
46 | 
47 |   max-width: 800px;
48 |   margin-top: 3rem;
49 | }
50 | 
51 | .card {
52 |   margin: 1rem;
53 |   flex-basis: 45%;
54 |   padding: 1.5rem;
55 |   text-align: left;
56 |   color: inherit;
57 |   text-decoration: none;
58 |   border: 1px solid #eaeaea;
59 |   border-radius: 10px;
60 |   transition:
61 |     color 0.15s ease,
62 |     border-color 0.15s ease;
63 | }
64 | 
65 | .card:hover,
66 | .card:focus,
67 | .card:active {
68 |   color: #0070f3;
69 |   border-color: #0070f3;
70 | }
71 | 
72 | .card h3 {
73 |   margin: 0 0 1rem 0;
74 |   font-size: 1.5rem;
75 | }
76 | 
77 | .card p {
78 |   margin: 0;
79 |   font-size: 1.25rem;
80 |   line-height: 1.5;
81 | }
82 | 
83 | .logo {
84 |   height: 1em;
85 | }
86 | 
87 | @media (max-width: 600px) {
88 |   .grid {
89 |     width: 100%;
90 |     flex-direction: column;
91 |   }
92 | }
93 | 


--------------------------------------------------------------------------------
/basics/navigate-between-pages-starter/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     Inter,
 7 |     -apple-system,
 8 |     BlinkMacSystemFont,
 9 |     Segoe UI,
10 |     Roboto,
11 |     Oxygen,
12 |     Ubuntu,
13 |     Cantarell,
14 |     Fira Sans,
15 |     Droid Sans,
16 |     Helvetica Neue,
17 |     sans-serif;
18 | }
19 | 
20 | a {
21 |   color: inherit;
22 |   text-decoration: none;
23 | }
24 | 
25 | * {
26 |   box-sizing: border-box;
27 | }
28 | 
29 | img {
30 |   max-width: 100%;
31 |   height: auto;
32 | }
33 | 


--------------------------------------------------------------------------------
/basics/snippets/link-classname-example.js:
--------------------------------------------------------------------------------
 1 | // Example: Adding className with <Link>
 2 | import Link from 'next/link';
 3 | 
 4 | export default function LinkClassnameExample() {
 5 |   return (
 6 |     <Link href="/" className="foo" target="_blank" rel="noopener noreferrer">
 7 |       Hello World
 8 |     </Link>
 9 |   );
10 | }
11 | 
12 | // Take a look at https://nextjs.org/docs/api-reference/next/link
13 | // to learn more!
14 | 


--------------------------------------------------------------------------------
/basics/typescript-final/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | .env*
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 


--------------------------------------------------------------------------------
/basics/typescript-final/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 | 


--------------------------------------------------------------------------------
/basics/typescript-final/README.md:
--------------------------------------------------------------------------------
1 | This is a starter template for [Learn Next.js](https://nextjs.org/learn).
2 | 


--------------------------------------------------------------------------------
/basics/typescript-final/components/date.tsx:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns';
2 | 
3 | export default function Date({ dateString }: { dateString: string }) {
4 |   const date = parseISO(dateString);
5 |   return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
6 | }
7 | 


--------------------------------------------------------------------------------
/basics/typescript-final/components/layout.module.css:
--------------------------------------------------------------------------------
 1 | .container {
 2 |   max-width: 36rem;
 3 |   padding: 0 1rem;
 4 |   margin: 3rem auto 6rem;
 5 | }
 6 | 
 7 | .header {
 8 |   display: flex;
 9 |   flex-direction: column;
10 |   align-items: center;
11 | }
12 | 
13 | .backToHome {
14 |   margin: 3rem 0 0;
15 | }
16 | 


--------------------------------------------------------------------------------
/basics/typescript-final/components/layout.tsx:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Image from 'next/image';
 3 | import styles from './layout.module.css';
 4 | import utilStyles from '../styles/utils.module.css';
 5 | import Link from 'next/link';
 6 | 
 7 | const name = '[Your Name]';
 8 | export const siteTitle = 'Next.js Sample Website';
 9 | 
10 | export default function Layout({
11 |   children,
12 |   home,
13 | }: {
14 |   children: React.ReactNode;
15 |   home?: boolean;
16 | }) {
17 |   return (
18 |     <div className={styles.container}>
19 |       <Head>
20 |         <link rel="icon" href="/favicon.ico" />
21 |         <meta
22 |           name="description"
23 |           content="Learn how to build a personal website using Next.js"
24 |         />
25 |         <meta
26 |           property="og:image"
27 |           content={`https://og-image.vercel.app/${encodeURI(
28 |             siteTitle,
29 |           )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
30 |         />
31 |         <meta name="og:title" content={siteTitle} />
32 |         <meta name="twitter:card" content="summary_large_image" />
33 |       </Head>
34 |       <header className={styles.header}>
35 |         {home ? (
36 |           <>
37 |             <Image
38 |               priority
39 |               src="/images/profile.jpg"
40 |               className={utilStyles.borderCircle}
41 |               height={144}
42 |               width={144}
43 |               alt={name}
44 |             />
45 |             <h1 className={utilStyles.heading2Xl}>{name}</h1>
46 |           </>
47 |         ) : (
48 |           <>
49 |             <Link href="/">
50 |               <Image
51 |                 priority
52 |                 src="/images/profile.jpg"
53 |                 className={utilStyles.borderCircle}
54 |                 height={108}
55 |                 width={108}
56 |                 alt={name}
57 |               />
58 |             </Link>
59 |             <h2 className={utilStyles.headingLg}>
60 |               <Link href="/" className={utilStyles.colorInherit}>
61 |                 {name}
62 |               </Link>
63 |             </h2>
64 |           </>
65 |         )}
66 |       </header>
67 |       <main>{children}</main>
68 |       {!home && (
69 |         <div className={styles.backToHome}>
70 |           <Link href="/">← Back to home</Link>
71 |         </div>
72 |       )}
73 |     </div>
74 |   );
75 | }
76 | 


--------------------------------------------------------------------------------
/basics/typescript-final/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'remark-html' {
2 |   const html: any;
3 |   export default html;
4 | }
5 | 


--------------------------------------------------------------------------------
/basics/typescript-final/lib/posts.ts:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | import path from 'path';
 3 | import matter from 'gray-matter';
 4 | import { remark } from 'remark';
 5 | import html from 'remark-html';
 6 | 
 7 | const postsDirectory = path.join(process.cwd(), 'posts');
 8 | 
 9 | export function getSortedPostsData() {
10 |   // Get file names under /posts
11 |   const fileNames = fs.readdirSync(postsDirectory);
12 |   const allPostsData = fileNames.map((fileName) => {
13 |     // Remove ".md" from file name to get id
14 |     const id = fileName.replace(/\.md$/, '');
15 | 
16 |     // Read markdown file as string
17 |     const fullPath = path.join(postsDirectory, fileName);
18 |     const fileContents = fs.readFileSync(fullPath, 'utf8');
19 | 
20 |     // Use gray-matter to parse the post metadata section
21 |     const matterResult = matter(fileContents);
22 | 
23 |     // Combine the data with the id
24 |     return {
25 |       id,
26 |       ...(matterResult.data as { date: string; title: string }),
27 |     };
28 |   });
29 |   // Sort posts by date
30 |   return allPostsData.sort((a, b) => {
31 |     if (a.date < b.date) {
32 |       return 1;
33 |     } else {
34 |       return -1;
35 |     }
36 |   });
37 | }
38 | 
39 | export function getAllPostIds() {
40 |   const fileNames = fs.readdirSync(postsDirectory);
41 |   return fileNames.map((fileName) => {
42 |     return {
43 |       params: {
44 |         id: fileName.replace(/\.md$/, ''),
45 |       },
46 |     };
47 |   });
48 | }
49 | 
50 | export async function getPostData(id: string) {
51 |   const fullPath = path.join(postsDirectory, `${id}.md`);
52 |   const fileContents = fs.readFileSync(fullPath, 'utf8');
53 | 
54 |   // Use gray-matter to parse the post metadata section
55 |   const matterResult = matter(fileContents);
56 | 
57 |   // Use remark to convert markdown into HTML string
58 |   const processedContent = await remark()
59 |     .use(html)
60 |     .process(matterResult.content);
61 |   const contentHtml = processedContent.toString();
62 | 
63 |   // Combine the data with the id and contentHtml
64 |   return {
65 |     id,
66 |     contentHtml,
67 |     ...(matterResult.data as { date: string; title: string }),
68 |   };
69 | }
70 | 


--------------------------------------------------------------------------------
/basics/typescript-final/next-env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="next" />
2 | /// <reference types="next/image-types/global" />
3 | 
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 | 


--------------------------------------------------------------------------------
/basics/typescript-final/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "date-fns": "^2.29.3",
10 |     "gray-matter": "^4.0.3",
11 |     "next": "^13.0.2",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "remark": "^14.0.2",
15 |     "remark-html": "^15.0.1"
16 |   },
17 |   "devDependencies": {
18 |     "@types/node": "^18.11.9",
19 |     "@types/react": "^18.0.25",
20 |     "typescript": "^4.8.4"
21 |   },
22 |   "engines": {
23 |     "node": ">=18"
24 |   }
25 | }
26 | 


--------------------------------------------------------------------------------
/basics/typescript-final/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | import { AppProps } from 'next/app';
3 | 
4 | export default function App({ Component, pageProps }: AppProps) {
5 |   return <Component {...pageProps} />;
6 | }
7 | 


--------------------------------------------------------------------------------
/basics/typescript-final/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | 
3 | export default (_: NextApiRequest, res: NextApiResponse) => {
4 |   res.status(200).json({ text: 'Hello' });
5 | };
6 | 


--------------------------------------------------------------------------------
/basics/typescript-final/pages/index.tsx:
--------------------------------------------------------------------------------
 1 | import Head from 'next/head';
 2 | import Layout, { siteTitle } from '../components/layout';
 3 | import utilStyles from '../styles/utils.module.css';
 4 | import { getSortedPostsData } from '../lib/posts';
 5 | import Link from 'next/link';
 6 | import Date from '../components/date';
 7 | import { GetStaticProps } from 'next';
 8 | 
 9 | export default function Home({
10 |   allPostsData,
11 | }: {
12 |   allPostsData: {
13 |     date: string;
14 |     title: string;
15 |     id: string;
16 |   }[];
17 | }) {
18 |   return (
19 |     <Layout home>
20 |       <Head>
21 |         <title>{siteTitle}</title>
22 |       </Head>
23 |       <section className={utilStyles.headingMd}>
24 |         <p>[Your Self Introduction]</p>
25 |         <p>
26 |           (This is a sample website - you’ll be building a site like this in{' '}
27 |           <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
28 |         </p>
29 |       </section>
30 |       <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
31 |         <h2 className={utilStyles.headingLg}>Blog</h2>
32 |         <ul className={utilStyles.list}>
33 |           {allPostsData.map(({ id, date, title }) => (
34 |             <li className={utilStyles.listItem} key={id}>
35 |               <Link href={`/posts/${id}`}>{title}</Link>
36 |               <br />
37 |               <small className={utilStyles.lightText}>
38 |                 <Date dateString={date} />
39 |               </small>
40 |             </li>
41 |           ))}
42 |         </ul>
43 |       </section>
44 |     </Layout>
45 |   );
46 | }
47 | 
48 | export const getStaticProps: GetStaticProps = async () => {
49 |   const allPostsData = getSortedPostsData();
50 |   return {
51 |     props: {
52 |       allPostsData,
53 |     },
54 |   };
55 | };
56 | 


--------------------------------------------------------------------------------
/basics/typescript-final/pages/posts/[id].tsx:
--------------------------------------------------------------------------------
 1 | import Layout from '../../components/layout';
 2 | import { getAllPostIds, getPostData } from '../../lib/posts';
 3 | import Head from 'next/head';
 4 | import Date from '../../components/date';
 5 | import utilStyles from '../../styles/utils.module.css';
 6 | import { GetStaticProps, GetStaticPaths } from 'next';
 7 | 
 8 | export default function Post({
 9 |   postData,
10 | }: {
11 |   postData: {
12 |     title: string;
13 |     date: string;
14 |     contentHtml: string;
15 |   };
16 | }) {
17 |   return (
18 |     <Layout>
19 |       <Head>
20 |         <title>{postData.title}</title>
21 |       </Head>
22 |       <article>
23 |         <h1 className={utilStyles.headingXl}>{postData.title}</h1>
24 |         <div className={utilStyles.lightText}>
25 |           <Date dateString={postData.date} />
26 |         </div>
27 |         <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
28 |       </article>
29 |     </Layout>
30 |   );
31 | }
32 | 
33 | export const getStaticPaths: GetStaticPaths = async () => {
34 |   const paths = getAllPostIds();
35 |   return {
36 |     paths,
37 |     fallback: false,
38 |   };
39 | };
40 | 
41 | export const getStaticProps: GetStaticProps = async ({ params }) => {
42 |   const postData = await getPostData(params?.id as string);
43 |   return {
44 |     props: {
45 |       postData,
46 |     },
47 |   };
48 | };
49 | 


--------------------------------------------------------------------------------
/basics/typescript-final/posts/pre-rendering.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'Two Forms of Pre-rendering'
 3 | date: '2022-01-01'
 4 | ---
 5 | 
 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
 7 | 
 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
10 | 
11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
12 | 


--------------------------------------------------------------------------------
/basics/typescript-final/posts/ssg-ssr.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: 'When to Use Static Generation v.s. Server-side Rendering'
 3 | date: '2022-01-02'
 4 | ---
 5 | 
 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
 7 | 
 8 | You can use Static Generation for many types of pages, including:
 9 | 
10 | - Marketing pages
11 | - Blog posts
12 | - E-commerce product listings
13 | - Help and documentation
14 | 
15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
16 | 
17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
18 | 
19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
20 | 


--------------------------------------------------------------------------------
/basics/typescript-final/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/typescript-final/public/favicon.ico


--------------------------------------------------------------------------------
/basics/typescript-final/public/images/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/basics/typescript-final/public/images/profile.jpg


--------------------------------------------------------------------------------
/basics/typescript-final/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     -apple-system,
 7 |     BlinkMacSystemFont,
 8 |     Segoe UI,
 9 |     Roboto,
10 |     Oxygen,
11 |     Ubuntu,
12 |     Cantarell,
13 |     Fira Sans,
14 |     Droid Sans,
15 |     Helvetica Neue,
16 |     sans-serif;
17 |   line-height: 1.6;
18 |   font-size: 18px;
19 | }
20 | 
21 | * {
22 |   box-sizing: border-box;
23 | }
24 | 
25 | a {
26 |   color: #0070f3;
27 |   text-decoration: none;
28 | }
29 | 
30 | a:hover {
31 |   text-decoration: underline;
32 | }
33 | 
34 | img {
35 |   max-width: 100%;
36 |   display: block;
37 | }
38 | 


--------------------------------------------------------------------------------
/basics/typescript-final/styles/utils.module.css:
--------------------------------------------------------------------------------
 1 | .heading2Xl {
 2 |   font-size: 2.5rem;
 3 |   line-height: 1.2;
 4 |   font-weight: 800;
 5 |   letter-spacing: -0.05rem;
 6 |   margin: 1rem 0;
 7 | }
 8 | 
 9 | .headingXl {
10 |   font-size: 2rem;
11 |   line-height: 1.3;
12 |   font-weight: 800;
13 |   letter-spacing: -0.05rem;
14 |   margin: 1rem 0;
15 | }
16 | 
17 | .headingLg {
18 |   font-size: 1.5rem;
19 |   line-height: 1.4;
20 |   margin: 1rem 0;
21 | }
22 | 
23 | .headingMd {
24 |   font-size: 1.2rem;
25 |   line-height: 1.5;
26 | }
27 | 
28 | .borderCircle {
29 |   border-radius: 9999px;
30 | }
31 | 
32 | .colorInherit {
33 |   color: inherit;
34 | }
35 | 
36 | .padding1px {
37 |   padding-top: 1px;
38 | }
39 | 
40 | .list {
41 |   list-style: none;
42 |   padding: 0;
43 |   margin: 0;
44 | }
45 | 
46 | .listItem {
47 |   margin: 0 0 1.25rem;
48 | }
49 | 
50 | .lightText {
51 |   color: #666;
52 | }
53 | 


--------------------------------------------------------------------------------
/basics/typescript-final/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "es5",
 4 |     "lib": ["dom", "dom.iterable", "esnext"],
 5 |     "allowJs": true,
 6 |     "skipLibCheck": true,
 7 |     "strict": true,
 8 |     "forceConsistentCasingInFileNames": true,
 9 |     "noEmit": true,
10 |     "esModuleInterop": true,
11 |     "module": "esnext",
12 |     "moduleResolution": "node",
13 |     "resolveJsonModule": true,
14 |     "isolatedModules": true,
15 |     "jsx": "preserve",
16 |     "incremental": true
17 |   },
18 |   "exclude": ["node_modules"],
19 |   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
20 | }
21 | 


--------------------------------------------------------------------------------
/dashboard/README.md:
--------------------------------------------------------------------------------
1 | # Next.js App Router Course - Build a Dashboard
2 | 
3 | This repository contains the starter templates for the [Next.js App Router Course](https://nextjs.org/learn), separated by chapters.
4 | 
5 | For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.
6 | 


--------------------------------------------------------------------------------
/dashboard/final-example/.env.example:
--------------------------------------------------------------------------------
 1 | # Copy from .env.local on the Vercel dashboard
 2 | # https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
 3 | POSTGRES_URL=
 4 | POSTGRES_PRISMA_URL=
 5 | POSTGRES_URL_NON_POOLING=
 6 | POSTGRES_USER=
 7 | POSTGRES_HOST=
 8 | POSTGRES_PASSWORD=
 9 | POSTGRES_DATABASE=
10 | 
11 | # `openssl rand -base64 32`
12 | AUTH_SECRET=
13 | AUTH_URL=http://localhost:3000/api/auth


--------------------------------------------------------------------------------
/dashboard/final-example/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | *.pem
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 
27 | # local env files
28 | .env*.local
29 | .env
30 | 
31 | # vercel
32 | .vercel
33 | 
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | 


--------------------------------------------------------------------------------
/dashboard/final-example/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js App Router Course - Final
2 | 
3 | This is the final template for the Next.js App Router Course. It contains the final code for the dashboard application.
4 | 
5 | For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.
6 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/(overview)/loading.tsx:
--------------------------------------------------------------------------------
1 | import DashboardSkeleton from '@/app/ui/skeletons';
2 | 
3 | export default function Loading() {
4 |   return <DashboardSkeleton />;
5 | }
6 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/(overview)/page.tsx:
--------------------------------------------------------------------------------
 1 | import CardWrapper from '@/app/ui/dashboard/cards';
 2 | import RevenueChart from '@/app/ui/dashboard/revenue-chart';
 3 | import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
 4 | import { lusitana } from '@/app/ui/fonts';
 5 | import { Suspense } from 'react';
 6 | import {
 7 |   RevenueChartSkeleton,
 8 |   LatestInvoicesSkeleton,
 9 |   CardsSkeleton,
10 | } from '@/app/ui/skeletons';
11 | 
12 | export default async function Page() {
13 |   return (
14 |     <main>
15 |       <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
16 |         Dashboard
17 |       </h1>
18 |       <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
19 |         <Suspense fallback={<CardsSkeleton />}>
20 |           <CardWrapper />
21 |         </Suspense>
22 |       </div>
23 |       <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
24 |         <Suspense fallback={<RevenueChartSkeleton />}>
25 |           <RevenueChart />
26 |         </Suspense>
27 |         <Suspense fallback={<LatestInvoicesSkeleton />}>
28 |           <LatestInvoices />
29 |         </Suspense>
30 |       </div>
31 |     </main>
32 |   );
33 | }
34 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/customers/page.tsx:
--------------------------------------------------------------------------------
 1 | import { fetchFilteredCustomers } from '@/app/lib/data';
 2 | import CustomersTable from '@/app/ui/customers/table';
 3 | import { Metadata } from 'next';
 4 | 
 5 | export const metadata: Metadata = {
 6 |   title: 'Customers',
 7 | };
 8 | 
 9 | export default async function Page(props: {
10 |   searchParams?: Promise<{
11 |     query?: string;
12 |     page?: string;
13 |   }>;
14 | }) {
15 |   const searchParams = await props.searchParams;
16 |   const query = searchParams?.query || '';
17 | 
18 |   const customers = await fetchFilteredCustomers(query);
19 | 
20 |   return (
21 |     <main>
22 |       <CustomersTable customers={customers} />
23 |     </main>
24 |   );
25 | }
26 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/invoices/[id]/edit/not-found.tsx:
--------------------------------------------------------------------------------
 1 | import Link from 'next/link';
 2 | import { FaceFrownIcon } from '@heroicons/react/24/outline';
 3 | 
 4 | export default function NotFound() {
 5 |   return (
 6 |     <main className="flex h-full flex-col items-center justify-center gap-2">
 7 |       <FaceFrownIcon className="w-10 text-gray-400" />
 8 |       <h2 className="text-xl font-semibold">404 Not Found</h2>
 9 |       <p>Could not find the requested invoice.</p>
10 |       <Link
11 |         href="/dashboard/invoices"
12 |         className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
13 |       >
14 |         Go Back
15 |       </Link>
16 |     </main>
17 |   );
18 | }
19 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/invoices/[id]/edit/page.tsx:
--------------------------------------------------------------------------------
 1 | import Form from '@/app/ui/invoices/edit-form';
 2 | import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
 3 | import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
 4 | import { notFound } from 'next/navigation';
 5 | import { Metadata } from 'next';
 6 | 
 7 | export const metadata: Metadata = {
 8 |   title: 'Edit Invoice',
 9 | };
10 | 
11 | export default async function Page(props: { params: Promise<{ id: string }> }) {
12 |   const params = await props.params;
13 |   const id = params.id;
14 |   const [invoice, customers] = await Promise.all([
15 |     fetchInvoiceById(id),
16 |     fetchCustomers(),
17 |   ]);
18 | 
19 |   if (!invoice) {
20 |     notFound();
21 |   }
22 | 
23 |   return (
24 |     <main>
25 |       <Breadcrumbs
26 |         breadcrumbs={[
27 |           { label: 'Invoices', href: '/dashboard/invoices' },
28 |           {
29 |             label: 'Edit Invoice',
30 |             href: `/dashboard/invoices/${id}/edit`,
31 |             active: true,
32 |           },
33 |         ]}
34 |       />
35 |       <Form invoice={invoice} customers={customers} />
36 |     </main>
37 |   );
38 | }
39 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/invoices/create/page.tsx:
--------------------------------------------------------------------------------
 1 | import { fetchCustomers } from '@/app/lib/data';
 2 | import Form from '@/app/ui/invoices/create-form';
 3 | import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
 4 | import { Metadata } from 'next';
 5 | 
 6 | export const metadata: Metadata = {
 7 |   title: 'Create Invoice',
 8 | };
 9 | 
10 | export default async function Page() {
11 |   const customers = await fetchCustomers();
12 | 
13 |   return (
14 |     <main>
15 |       <Breadcrumbs
16 |         breadcrumbs={[
17 |           { label: 'Invoices', href: '/dashboard/invoices' },
18 |           {
19 |             label: 'Create Invoice',
20 |             href: '/dashboard/invoices/create',
21 |             active: true,
22 |           },
23 |         ]}
24 |       />
25 |       <Form customers={customers} />
26 |     </main>
27 |   );
28 | }
29 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/invoices/error.tsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import { useEffect } from 'react';
 4 | 
 5 | export default function Error({
 6 |   error,
 7 |   reset,
 8 | }: {
 9 |   error: Error & { digest?: string };
10 |   reset: () => void;
11 | }) {
12 |   useEffect(() => {
13 |     // Optionally log the error to an error reporting service
14 |     console.error(error);
15 |   }, [error]);
16 | 
17 |   return (
18 |     <main className="flex h-full flex-col items-center justify-center">
19 |       <h2 className="text-center">Something went wrong!</h2>
20 |       <button
21 |         className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
22 |         onClick={
23 |           // Attempt to recover by trying to re-render the invoices route
24 |           () => reset()
25 |         }
26 |       >
27 |         Try again
28 |       </button>
29 |     </main>
30 |   );
31 | }
32 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/invoices/page.tsx:
--------------------------------------------------------------------------------
 1 | import Pagination from '@/app/ui/invoices/pagination';
 2 | import Search from '@/app/ui/search';
 3 | import Table from '@/app/ui/invoices/table';
 4 | import { CreateInvoice } from '@/app/ui/invoices/buttons';
 5 | import { lusitana } from '@/app/ui/fonts';
 6 | import { InvoicesTableSkeleton } from '@/app/ui/skeletons';
 7 | import { Suspense } from 'react';
 8 | import { fetchInvoicesPages } from '@/app/lib/data';
 9 | import { Metadata } from 'next';
10 | 
11 | export const metadata: Metadata = {
12 |   title: 'Invoices',
13 | };
14 | 
15 | export default async function Page(props: {
16 |   searchParams?: Promise<{
17 |     query?: string;
18 |     page?: string;
19 |   }>;
20 | }) {
21 |   const searchParams = await props.searchParams;
22 |   const query = searchParams?.query || '';
23 |   const currentPage = Number(searchParams?.page) || 1;
24 | 
25 |   const totalPages = await fetchInvoicesPages(query);
26 | 
27 |   return (
28 |     <div className="w-full">
29 |       <div className="flex w-full items-center justify-between">
30 |         <h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
31 |       </div>
32 |       <div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
33 |         <Search placeholder="Search invoices..." />
34 |         <CreateInvoice />
35 |       </div>
36 |       <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
37 |         <Table query={query} currentPage={currentPage} />
38 |       </Suspense>
39 |       <div className="mt-5 flex w-full justify-center">
40 |         <Pagination totalPages={totalPages} />
41 |       </div>
42 |     </div>
43 |   );
44 | }
45 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
 1 | import SideNav from '@/app/ui/dashboard/sidenav';
 2 | 
 3 | export default function Layout({ children }: { children: React.ReactNode }) {
 4 |   return (
 5 |     <div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
 6 |       <div className="w-full flex-none md:w-64">
 7 |         <SideNav />
 8 |       </div>
 9 |       <div className="grow p-6 md:overflow-y-auto md:p-12">{children}</div>
10 |     </div>
11 |   );
12 | }
13 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/app/favicon.ico


--------------------------------------------------------------------------------
/dashboard/final-example/app/layout.tsx:
--------------------------------------------------------------------------------
 1 | import '@/app/ui/global.css';
 2 | import { inter } from '@/app/ui/fonts';
 3 | import { Metadata } from 'next';
 4 | 
 5 | export const metadata: Metadata = {
 6 |   title: {
 7 |     template: '%s | Acme Dashboard',
 8 |     default: 'Acme Dashboard',
 9 |   },
10 |   description: 'The official Next.js Learn Dashboard built with App Router.',
11 |   metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
12 | };
13 | export default function RootLayout({
14 |   children,
15 | }: {
16 |   children: React.ReactNode;
17 | }) {
18 |   return (
19 |     <html lang="en">
20 |       <body className={`${inter.className} antialiased`}>{children}</body>
21 |     </html>
22 |   );
23 | }
24 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/lib/definitions.ts:
--------------------------------------------------------------------------------
 1 | // This file contains type definitions for your data.
 2 | // It describes the shape of the data, and what data type each property should accept.
 3 | // For simplicity of teaching, we're manually defining these types.
 4 | // However, these types are generated automatically if you're using an ORM such as Prisma.
 5 | export type User = {
 6 |   id: string;
 7 |   name: string;
 8 |   email: string;
 9 |   password: string;
10 | };
11 | 
12 | export type Customer = {
13 |   id: string;
14 |   name: string;
15 |   email: string;
16 |   image_url: string;
17 | };
18 | 
19 | export type Invoice = {
20 |   id: string;
21 |   customer_id: string;
22 |   amount: number;
23 |   date: string;
24 |   // In TypeScript, this is called a string union type.
25 |   // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
26 |   status: 'pending' | 'paid';
27 | };
28 | 
29 | export type Revenue = {
30 |   month: string;
31 |   revenue: number;
32 | };
33 | 
34 | export type LatestInvoice = {
35 |   id: string;
36 |   name: string;
37 |   image_url: string;
38 |   email: string;
39 |   amount: string;
40 | };
41 | 
42 | // The database returns a number for amount, but we later format it to a string with the formatCurrency function
43 | export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & {
44 |   amount: number;
45 | };
46 | 
47 | export type InvoicesTable = {
48 |   id: string;
49 |   customer_id: string;
50 |   name: string;
51 |   email: string;
52 |   image_url: string;
53 |   date: string;
54 |   amount: number;
55 |   status: 'pending' | 'paid';
56 | };
57 | 
58 | export type CustomersTableType = {
59 |   id: string;
60 |   name: string;
61 |   email: string;
62 |   image_url: string;
63 |   total_invoices: number;
64 |   total_pending: number;
65 |   total_paid: number;
66 | };
67 | 
68 | export type FormattedCustomersTable = {
69 |   id: string;
70 |   name: string;
71 |   email: string;
72 |   image_url: string;
73 |   total_invoices: number;
74 |   total_pending: string;
75 |   total_paid: string;
76 | };
77 | 
78 | export type CustomerField = {
79 |   id: string;
80 |   name: string;
81 | };
82 | 
83 | export type InvoiceForm = {
84 |   id: string;
85 |   customer_id: string;
86 |   amount: number;
87 |   status: 'pending' | 'paid';
88 | };
89 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/lib/utils.ts:
--------------------------------------------------------------------------------
 1 | import { Revenue } from './definitions';
 2 | 
 3 | export const formatCurrency = (amount: number) => {
 4 |   return (amount / 100).toLocaleString('en-US', {
 5 |     style: 'currency',
 6 |     currency: 'USD',
 7 |   });
 8 | };
 9 | 
10 | export const formatDateToLocal = (
11 |   dateStr: string,
12 |   locale: string = 'en-US',
13 | ) => {
14 |   const date = new Date(dateStr);
15 |   const options: Intl.DateTimeFormatOptions = {
16 |     day: 'numeric',
17 |     month: 'short',
18 |     year: 'numeric',
19 |   };
20 |   const formatter = new Intl.DateTimeFormat(locale, options);
21 |   return formatter.format(date);
22 | };
23 | 
24 | export const generateYAxis = (revenue: Revenue[]) => {
25 |   // Calculate what labels we need to display on the y-axis
26 |   // based on highest record and in 1000s
27 |   const yAxisLabels = [];
28 |   const highestRecord = Math.max(...revenue.map((month) => month.revenue));
29 |   const topLabel = Math.ceil(highestRecord / 1000) * 1000;
30 | 
31 |   for (let i = topLabel; i >= 0; i -= 1000) {
32 |     yAxisLabels.push(`${i / 1000}K`);
33 |   }
34 | 
35 |   return { yAxisLabels, topLabel };
36 | };
37 | 
38 | export const generatePagination = (currentPage: number, totalPages: number) => {
39 |   // If the total number of pages is 7 or less,
40 |   // display all pages without any ellipsis.
41 |   if (totalPages <= 7) {
42 |     return Array.from({ length: totalPages }, (_, i) => i + 1);
43 |   }
44 | 
45 |   // If the current page is among the first 3 pages,
46 |   // show the first 3, an ellipsis, and the last 2 pages.
47 |   if (currentPage <= 3) {
48 |     return [1, 2, 3, '...', totalPages - 1, totalPages];
49 |   }
50 | 
51 |   // If the current page is among the last 3 pages,
52 |   // show the first 2, an ellipsis, and the last 3 pages.
53 |   if (currentPage >= totalPages - 2) {
54 |     return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
55 |   }
56 | 
57 |   // If the current page is somewhere in the middle,
58 |   // show the first page, an ellipsis, the current page and its neighbors,
59 |   // another ellipsis, and the last page.
60 |   return [
61 |     1,
62 |     '...',
63 |     currentPage - 1,
64 |     currentPage,
65 |     currentPage + 1,
66 |     '...',
67 |     totalPages,
68 |   ];
69 | };
70 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/login/page.tsx:
--------------------------------------------------------------------------------
 1 | import AcmeLogo from '@/app/ui/acme-logo';
 2 | import LoginForm from '@/app/ui/login-form';
 3 | import { Suspense } from 'react';
 4 | 
 5 | export default function LoginPage() {
 6 |   return (
 7 |     <main className="flex items-center justify-center md:h-screen">
 8 |       <div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
 9 |         <div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
10 |           <div className="w-32 text-white md:w-36">
11 |             <AcmeLogo />
12 |           </div>
13 |         </div>
14 |         <Suspense>
15 |           <LoginForm />
16 |         </Suspense>
17 |       </div>
18 |     </main>
19 |   );
20 | }
21 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/app/opengraph-image.png


--------------------------------------------------------------------------------
/dashboard/final-example/app/page.tsx:
--------------------------------------------------------------------------------
 1 | import AcmeLogo from '@/app/ui/acme-logo';
 2 | import { ArrowRightIcon } from '@heroicons/react/24/outline';
 3 | import Link from 'next/link';
 4 | import { lusitana } from '@/app/ui/fonts';
 5 | import Image from 'next/image';
 6 | 
 7 | export default function Page() {
 8 |   return (
 9 |     <main className="flex min-h-screen flex-col p-6">
10 |       <div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
11 |         <AcmeLogo />
12 |       </div>
13 |       <div className="mt-4 flex grow flex-col gap-4 md:flex-row">
14 |         <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">
15 |           <p
16 |             className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}
17 |           >
18 |             <strong>Welcome to Acme.</strong> This is the example for the{' '}
19 |             <a href="https://nextjs.org/learn/" className="text-blue-500">
20 |               Next.js Learn Course
21 |             </a>
22 |             , brought to you by Vercel.
23 |           </p>
24 |           <Link
25 |             href="/login"
26 |             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"
27 |           >
28 |             <span>Log in</span> <ArrowRightIcon className="w-5 md:w-6" />
29 |           </Link>
30 |         </div>
31 |         <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
32 |           {/* Add Hero Images Here */}
33 |           <Image
34 |             src="/hero-desktop.png"
35 |             width={1000}
36 |             height={760}
37 |             alt="Screenshots of the dashboard project showing desktop version"
38 |             className="hidden md:block"
39 |           />
40 |           <Image
41 |             src="/hero-mobile.png"
42 |             width={560}
43 |             height={620}
44 |             alt="Screenshot of the dashboard project showing mobile version"
45 |             className="block md:hidden"
46 |           />
47 |         </div>
48 |       </div>
49 |     </main>
50 |   );
51 | }
52 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/query/route.ts:
--------------------------------------------------------------------------------
 1 | import postgres from 'postgres';
 2 | 
 3 | const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 4 | 
 5 | async function listInvoices() {
 6 |   const data = await sql`
 7 |     SELECT invoices.amount, customers.name
 8 |     FROM invoices
 9 |     JOIN customers ON invoices.customer_id = customers.id
10 |     WHERE invoices.amount = 666;
11 |   `;
12 | 
13 |   return data;
14 | }
15 | 
16 | export async function GET() {
17 |   // return Response.json({
18 |   //   message:
19 |   //     'Uncomment this file and remove this line. You can delete this file when you are finished.',
20 |   // });
21 |   try {
22 |     return Response.json(await listInvoices());
23 |   } catch (error) {
24 |     return Response.json({ error }, { status: 500 });
25 |   }
26 | }
27 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/acme-logo.tsx:
--------------------------------------------------------------------------------
 1 | import { GlobeAltIcon } from '@heroicons/react/24/outline';
 2 | import { lusitana } from '@/app/ui/fonts';
 3 | 
 4 | export default function AcmeLogo() {
 5 |   return (
 6 |     <div
 7 |       className={`${lusitana.className} flex flex-row items-center leading-none text-white`}
 8 |     >
 9 |       <GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
10 |       <p className="text-[44px] ">Acme</p>
11 |     </div>
12 |   );
13 | }
14 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/button.tsx:
--------------------------------------------------------------------------------
 1 | import clsx from 'clsx';
 2 | 
 3 | interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
 4 |   children: React.ReactNode;
 5 | }
 6 | 
 7 | export function Button({ children, className, ...rest }: ButtonProps) {
 8 |   return (
 9 |     <button
10 |       {...rest}
11 |       className={clsx(
12 |         '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',
13 |         className,
14 |       )}
15 |     >
16 |       {children}
17 |     </button>
18 |   );
19 | }
20 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/dashboard/cards.tsx:
--------------------------------------------------------------------------------
 1 | import {
 2 |   BanknotesIcon,
 3 |   ClockIcon,
 4 |   UserGroupIcon,
 5 |   InboxIcon,
 6 | } from '@heroicons/react/24/outline';
 7 | import { lusitana } from '@/app/ui/fonts';
 8 | import { fetchCardData } from '@/app/lib/data';
 9 | 
10 | const iconMap = {
11 |   collected: BanknotesIcon,
12 |   customers: UserGroupIcon,
13 |   pending: ClockIcon,
14 |   invoices: InboxIcon,
15 | };
16 | 
17 | export default async function CardWrapper() {
18 |   const {
19 |     numberOfInvoices,
20 |     numberOfCustomers,
21 |     totalPaidInvoices,
22 |     totalPendingInvoices,
23 |   } = await fetchCardData();
24 | 
25 |   return (
26 |     <>
27 |       <Card title="Collected" value={totalPaidInvoices} type="collected" />
28 |       <Card title="Pending" value={totalPendingInvoices} type="pending" />
29 |       <Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
30 |       <Card
31 |         title="Total Customers"
32 |         value={numberOfCustomers}
33 |         type="customers"
34 |       />
35 |     </>
36 |   );
37 | }
38 | 
39 | export function Card({
40 |   title,
41 |   value,
42 |   type,
43 | }: {
44 |   title: string;
45 |   value: number | string;
46 |   type: 'invoices' | 'customers' | 'pending' | 'collected';
47 | }) {
48 |   const Icon = iconMap[type];
49 | 
50 |   return (
51 |     <div className="rounded-xl bg-gray-50 p-2 shadow-sm">
52 |       <div className="flex p-4">
53 |         {Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
54 |         <h3 className="ml-2 text-sm font-medium">{title}</h3>
55 |       </div>
56 |       <p
57 |         className={`${lusitana.className}
58 |           truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
59 |       >
60 |         {value}
61 |       </p>
62 |     </div>
63 |   );
64 | }
65 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/dashboard/latest-invoices.tsx:
--------------------------------------------------------------------------------
 1 | import { ArrowPathIcon } from '@heroicons/react/24/outline';
 2 | import clsx from 'clsx';
 3 | import Image from 'next/image';
 4 | import { lusitana } from '@/app/ui/fonts';
 5 | import { fetchLatestInvoices } from '@/app/lib/data';
 6 | 
 7 | export default async function LatestInvoices() {
 8 |   const latestInvoices = await fetchLatestInvoices();
 9 | 
10 |   return (
11 |     <div className="flex w-full flex-col md:col-span-4">
12 |       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
13 |         Latest Invoices
14 |       </h2>
15 |       <div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
16 |         <div className="bg-white px-6">
17 |           {latestInvoices.map((invoice, i) => {
18 |             return (
19 |               <div
20 |                 key={invoice.id}
21 |                 className={clsx(
22 |                   'flex flex-row items-center justify-between py-4',
23 |                   {
24 |                     'border-t': i !== 0,
25 |                   },
26 |                 )}
27 |               >
28 |                 <div className="flex items-center">
29 |                   <Image
30 |                     src={invoice.image_url}
31 |                     alt={`${invoice.name}'s profile picture`}
32 |                     className="mr-4 rounded-full"
33 |                     width={32}
34 |                     height={32}
35 |                   />
36 |                   <div className="min-w-0">
37 |                     <p className="truncate text-sm font-semibold md:text-base">
38 |                       {invoice.name}
39 |                     </p>
40 |                     <p className="hidden text-sm text-gray-500 sm:block">
41 |                       {invoice.email}
42 |                     </p>
43 |                   </div>
44 |                 </div>
45 |                 <p
46 |                   className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
47 |                 >
48 |                   {invoice.amount}
49 |                 </p>
50 |               </div>
51 |             );
52 |           })}
53 |         </div>
54 |         <div className="flex items-center pb-2 pt-6">
55 |           <ArrowPathIcon className="h-5 w-5 text-gray-500" />
56 |           <h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
57 |         </div>
58 |       </div>
59 |     </div>
60 |   );
61 | }
62 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/dashboard/nav-links.tsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import {
 4 |   UserGroupIcon,
 5 |   HomeIcon,
 6 |   DocumentDuplicateIcon,
 7 | } from '@heroicons/react/24/outline';
 8 | import Link from 'next/link';
 9 | import { usePathname } from 'next/navigation';
10 | import clsx from 'clsx';
11 | 
12 | // Map of links to display in the side navigation.
13 | // Depending on the size of the application, this would be stored in a database.
14 | const links = [
15 |   { name: 'Home', href: '/dashboard', icon: HomeIcon },
16 |   {
17 |     name: 'Invoices',
18 |     href: '/dashboard/invoices',
19 |     icon: DocumentDuplicateIcon,
20 |   },
21 |   { name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
22 | ];
23 | 
24 | export default function NavLinks() {
25 |   const pathname = usePathname();
26 | 
27 |   return (
28 |     <>
29 |       {links.map((link) => {
30 |         const LinkIcon = link.icon;
31 |         return (
32 |           <Link
33 |             key={link.name}
34 |             href={link.href}
35 |             className={clsx(
36 |               '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',
37 |               {
38 |                 'bg-sky-100 text-blue-600': pathname === link.href,
39 |               },
40 |             )}
41 |           >
42 |             <LinkIcon className="w-6" />
43 |             <p className="hidden md:block">{link.name}</p>
44 |           </Link>
45 |         );
46 |       })}
47 |     </>
48 |   );
49 | }
50 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/dashboard/revenue-chart.tsx:
--------------------------------------------------------------------------------
 1 | import { generateYAxis } from '@/app/lib/utils';
 2 | import { CalendarIcon } from '@heroicons/react/24/outline';
 3 | import { lusitana } from '@/app/ui/fonts';
 4 | import { fetchRevenue } from '@/app/lib/data';
 5 | 
 6 | // This component is representational only.
 7 | // For data visualization UI, check out:
 8 | // https://www.tremor.so/
 9 | // https://www.chartjs.org/
10 | // https://airbnb.io/visx/
11 | 
12 | export default async function RevenueChart() {
13 |   const revenue = await fetchRevenue();
14 | 
15 |   const chartHeight = 350;
16 |   const { yAxisLabels, topLabel } = generateYAxis(revenue);
17 | 
18 |   if (!revenue || revenue.length === 0) {
19 |     return <p className="mt-4 text-gray-400">No data available.</p>;
20 |   }
21 | 
22 |   return (
23 |     <div className="w-full md:col-span-4">
24 |       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
25 |         Recent Revenue
26 |       </h2>
27 |       <div className="rounded-xl bg-gray-50 p-4">
28 |         <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">
29 |           {/* y-axis */}
30 |           <div
31 |             className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
32 |             style={{ height: `${chartHeight}px` }}
33 |           >
34 |             {yAxisLabels.map((label) => (
35 |               <p key={label}>{label}</p>
36 |             ))}
37 |           </div>
38 | 
39 |           {revenue.map((month) => (
40 |             <div key={month.month} className="flex flex-col items-center gap-2">
41 |               {/* bars */}
42 |               <div
43 |                 className="w-full rounded-md bg-blue-300"
44 |                 style={{
45 |                   height: `${(chartHeight / topLabel) * month.revenue}px`,
46 |                 }}
47 |               ></div>
48 |               {/* x-axis */}
49 |               <p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
50 |                 {month.month}
51 |               </p>
52 |             </div>
53 |           ))}
54 |         </div>
55 |         <div className="flex items-center pb-2 pt-6">
56 |           <CalendarIcon className="h-5 w-5 text-gray-500" />
57 |           <h3 className="ml-2 text-sm text-gray-500 ">Last 12 months</h3>
58 |         </div>
59 |       </div>
60 |     </div>
61 |   );
62 | }
63 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/dashboard/sidenav.tsx:
--------------------------------------------------------------------------------
 1 | import Link from 'next/link';
 2 | import NavLinks from '@/app/ui/dashboard/nav-links';
 3 | import AcmeLogo from '@/app/ui/acme-logo';
 4 | import { PowerIcon } from '@heroicons/react/24/outline';
 5 | import { signOut } from '@/auth';
 6 | 
 7 | export default function SideNav() {
 8 |   return (
 9 |     <div className="flex h-full flex-col px-3 py-4 md:px-2">
10 |       <Link
11 |         className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
12 |         href="/"
13 |       >
14 |         <div className="w-32 text-white md:w-40">
15 |           <AcmeLogo />
16 |         </div>
17 |       </Link>
18 |       <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
19 |         <NavLinks />
20 |         <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
21 |         <form
22 |           action={async () => {
23 |             'use server';
24 |             await signOut({ redirectTo: '/' });
25 |           }}
26 |         >
27 |           <button className="flex h-[48px] w-full 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">
28 |             <PowerIcon className="w-6" />
29 |             <div className="hidden md:block">Sign Out</div>
30 |           </button>
31 |         </form>
32 |       </div>
33 |     </div>
34 |   );
35 | }
36 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Inter, Lusitana } from 'next/font/google';
2 | 
3 | export const inter = Inter({ subsets: ['latin'] });
4 | 
5 | export const lusitana = Lusitana({
6 |   weight: ['400', '700'],
7 |   subsets: ['latin'],
8 | });
9 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/global.css:
--------------------------------------------------------------------------------
 1 | @tailwind base;
 2 | @tailwind components;
 3 | @tailwind utilities;
 4 | 
 5 | input[type='number'] {
 6 |   -moz-appearance: textfield;
 7 |   appearance: textfield;
 8 | }
 9 | 
10 | input[type='number']::-webkit-inner-spin-button {
11 |   -webkit-appearance: none;
12 |   margin: 0;
13 | }
14 | 
15 | input[type='number']::-webkit-outer-spin-button {
16 |   -webkit-appearance: none;
17 |   margin: 0;
18 | }
19 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/invoices/breadcrumbs.tsx:
--------------------------------------------------------------------------------
 1 | import { clsx } from 'clsx';
 2 | import Link from 'next/link';
 3 | import { lusitana } from '@/app/ui/fonts';
 4 | 
 5 | interface Breadcrumb {
 6 |   label: string;
 7 |   href: string;
 8 |   active?: boolean;
 9 | }
10 | 
11 | export default function Breadcrumbs({
12 |   breadcrumbs,
13 | }: {
14 |   breadcrumbs: Breadcrumb[];
15 | }) {
16 |   return (
17 |     <nav aria-label="Breadcrumb" className="mb-6 block">
18 |       <ol className={clsx(lusitana.className, 'flex text-xl md:text-2xl')}>
19 |         {breadcrumbs.map((breadcrumb, index) => (
20 |           <li
21 |             key={breadcrumb.href}
22 |             aria-current={breadcrumb.active}
23 |             className={clsx(
24 |               breadcrumb.active ? 'text-gray-900' : 'text-gray-500',
25 |             )}
26 |           >
27 |             <Link href={breadcrumb.href}>{breadcrumb.label}</Link>
28 |             {index < breadcrumbs.length - 1 ? (
29 |               <span className="mx-3 inline-block">/</span>
30 |             ) : null}
31 |           </li>
32 |         ))}
33 |       </ol>
34 |     </nav>
35 |   );
36 | }
37 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/invoices/buttons.tsx:
--------------------------------------------------------------------------------
 1 | import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
 2 | import Link from 'next/link';
 3 | import { deleteInvoice } from '@/app/lib/actions';
 4 | 
 5 | export function CreateInvoice() {
 6 |   return (
 7 |     <Link
 8 |       href="/dashboard/invoices/create"
 9 |       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"
10 |     >
11 |       <span className="hidden md:block">Create Invoice</span>{' '}
12 |       <PlusIcon className="h-5 md:ml-4" />
13 |     </Link>
14 |   );
15 | }
16 | 
17 | export function UpdateInvoice({ id }: { id: string }) {
18 |   return (
19 |     <Link
20 |       href={`/dashboard/invoices/${id}/edit`}
21 |       className="rounded-md border p-2 hover:bg-gray-100"
22 |     >
23 |       <PencilIcon className="w-5" />
24 |     </Link>
25 |   );
26 | }
27 | 
28 | export function DeleteInvoice({ id }: { id: string }) {
29 |   const deleteInvoiceWithId = deleteInvoice.bind(null, id);
30 | 
31 |   return (
32 |     <form action={deleteInvoiceWithId}>
33 |       <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
34 |         <span className="sr-only">Delete</span>
35 |         <TrashIcon className="w-5" />
36 |       </button>
37 |     </form>
38 |   );
39 | }
40 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/invoices/status.tsx:
--------------------------------------------------------------------------------
 1 | import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
 2 | import clsx from 'clsx';
 3 | 
 4 | export default function InvoiceStatus({ status }: { status: string }) {
 5 |   return (
 6 |     <span
 7 |       className={clsx(
 8 |         'inline-flex items-center rounded-full px-2 py-1 text-xs',
 9 |         {
10 |           'bg-gray-100 text-gray-500': status === 'pending',
11 |           'bg-green-500 text-white': status === 'paid',
12 |         },
13 |       )}
14 |     >
15 |       {status === 'pending' ? (
16 |         <>
17 |           Pending
18 |           <ClockIcon className="ml-1 w-4 text-gray-500" />
19 |         </>
20 |       ) : null}
21 |       {status === 'paid' ? (
22 |         <>
23 |           Paid
24 |           <CheckIcon className="ml-1 w-4 text-white" />
25 |         </>
26 |       ) : null}
27 |     </span>
28 |   );
29 | }
30 | 


--------------------------------------------------------------------------------
/dashboard/final-example/app/ui/search.tsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
 4 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
 5 | import { useDebouncedCallback } from 'use-debounce';
 6 | 
 7 | export default function Search({ placeholder }: { placeholder: string }) {
 8 |   const searchParams = useSearchParams();
 9 |   const { replace } = useRouter();
10 |   const pathname = usePathname();
11 | 
12 |   const handleSearch = useDebouncedCallback((term: string) => {
13 |     console.log(`Searching... ${term}`);
14 | 
15 |     const params = new URLSearchParams(searchParams);
16 | 
17 |     params.set('page', '1');
18 | 
19 |     if (term) {
20 |       params.set('query', term);
21 |     } else {
22 |       params.delete('query');
23 |     }
24 |     replace(`${pathname}?${params.toString()}`);
25 |   }, 300);
26 | 
27 |   return (
28 |     <div className="relative flex flex-1 flex-shrink-0">
29 |       <label htmlFor="search" className="sr-only">
30 |         Search
31 |       </label>
32 |       <input
33 |         className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
34 |         placeholder={placeholder}
35 |         onChange={(e) => {
36 |           handleSearch(e.target.value);
37 |         }}
38 |         defaultValue={searchParams.get('query')?.toString()}
39 |       />
40 |       <MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
41 |     </div>
42 |   );
43 | }
44 | 


--------------------------------------------------------------------------------
/dashboard/final-example/auth.config.ts:
--------------------------------------------------------------------------------
 1 | import type { NextAuthConfig } from 'next-auth';
 2 | 
 3 | export const authConfig = {
 4 |   pages: {
 5 |     signIn: '/login',
 6 |   },
 7 |   providers: [
 8 |     // added later in auth.ts since it requires bcrypt which is only compatible with Node.js
 9 |     // while this file is also used in non-Node.js environments
10 |   ],
11 |   callbacks: {
12 |     authorized({ auth, request: { nextUrl } }) {
13 |       const isLoggedIn = !!auth?.user;
14 |       const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
15 |       if (isOnDashboard) {
16 |         if (isLoggedIn) return true;
17 |         return false; // Redirect unauthenticated users to login page
18 |       } else if (isLoggedIn) {
19 |         return Response.redirect(new URL('/dashboard', nextUrl));
20 |       }
21 |       return true;
22 |     },
23 |   },
24 | } satisfies NextAuthConfig;
25 | 


--------------------------------------------------------------------------------
/dashboard/final-example/auth.ts:
--------------------------------------------------------------------------------
 1 | import NextAuth from 'next-auth';
 2 | import Credentials from 'next-auth/providers/credentials';
 3 | import bcrypt from 'bcrypt';
 4 | import postgres from 'postgres';
 5 | import { z } from 'zod';
 6 | import type { User } from '@/app/lib/definitions';
 7 | import { authConfig } from './auth.config';
 8 | 
 9 | const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
10 | 
11 | async function getUser(email: string): Promise<User | undefined> {
12 |   try {
13 |     const user = await sql<User[]>`SELECT * FROM users WHERE email=${email}`;
14 |     return user[0];
15 |   } catch (error) {
16 |     console.error('Failed to fetch user:', error);
17 |     throw new Error('Failed to fetch user.');
18 |   }
19 | }
20 | 
21 | export const { auth, signIn, signOut } = NextAuth({
22 |   ...authConfig,
23 |   providers: [
24 |     Credentials({
25 |       async authorize(credentials) {
26 |         const parsedCredentials = z
27 |           .object({ email: z.string().email(), password: z.string().min(6) })
28 |           .safeParse(credentials);
29 | 
30 |         if (parsedCredentials.success) {
31 |           const { email, password } = parsedCredentials.data;
32 | 
33 |           const user = await getUser(email);
34 |           if (!user) return null;
35 | 
36 |           const passwordsMatch = await bcrypt.compare(password, user.password);
37 |           if (passwordsMatch) return user;
38 |         }
39 | 
40 |         console.log('Invalid credentials');
41 |         return null;
42 |       },
43 |     }),
44 |   ],
45 | });
46 | 


--------------------------------------------------------------------------------
/dashboard/final-example/middleware.ts:
--------------------------------------------------------------------------------
 1 | import NextAuth from 'next-auth';
 2 | import { authConfig } from './auth.config';
 3 | 
 4 | export default NextAuth(authConfig).auth;
 5 | 
 6 | export const config = {
 7 |   // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
 8 |   matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
 9 | };
10 | 


--------------------------------------------------------------------------------
/dashboard/final-example/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 | 
3 | const nextConfig: NextConfig = {
4 |   /* config options here */
5 | };
6 | 
7 | export default nextConfig;
8 | 


--------------------------------------------------------------------------------
/dashboard/final-example/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "@heroicons/react": "^2.2.0",
10 |     "@tailwindcss/forms": "^0.5.10",
11 |     "autoprefixer": "10.4.20",
12 |     "bcrypt": "^5.1.1",
13 |     "clsx": "^2.1.1",
14 |     "next": "latest",
15 |     "next-auth": "5.0.0-beta.25",
16 |     "postcss": "8.5.1",
17 |     "postgres": "^3.4.6",
18 |     "react": "latest",
19 |     "react-dom": "latest",
20 |     "tailwindcss": "3.4.17",
21 |     "typescript": "5.7.3",
22 |     "use-debounce": "^10.0.4",
23 |     "zod": "^3.25.17"
24 |   },
25 |   "devDependencies": {
26 |     "@types/bcrypt": "^5.0.2",
27 |     "@types/node": "22.10.7",
28 |     "@types/react": "19.0.7",
29 |     "@types/react-dom": "19.0.3"
30 |   },
31 |   "pnpm": {
32 |     "onlyBuiltDependencies": [
33 |       "bcrypt",
34 |       "sharp"
35 |     ]
36 |   }
37 | }
38 | 


--------------------------------------------------------------------------------
/dashboard/final-example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | };
7 | 


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/amy-burns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/amy-burns.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/balazs-orban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/balazs-orban.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/delba-de-oliveira.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/delba-de-oliveira.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/evil-rabbit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/evil-rabbit.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/lee-robinson.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/lee-robinson.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/customers/michael-novotny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/customers/michael-novotny.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/hero-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/hero-desktop.png


--------------------------------------------------------------------------------
/dashboard/final-example/public/hero-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/final-example/public/hero-mobile.png


--------------------------------------------------------------------------------
/dashboard/final-example/tailwind.config.ts:
--------------------------------------------------------------------------------
 1 | import type { Config } from 'tailwindcss';
 2 | 
 3 | const config: Config = {
 4 |   content: [
 5 |     './pages/**/*.{js,ts,jsx,tsx,mdx}',
 6 |     './components/**/*.{js,ts,jsx,tsx,mdx}',
 7 |     './app/**/*.{js,ts,jsx,tsx,mdx}',
 8 |   ],
 9 |   theme: {
10 |     extend: {
11 |       gridTemplateColumns: {
12 |         '13': 'repeat(13, minmax(0, 1fr))',
13 |       },
14 |       colors: {
15 |         blue: {
16 |           400: '#2589FE',
17 |           500: '#0070F3',
18 |           600: '#2F6FEB',
19 |         },
20 |       },
21 |     },
22 |     keyframes: {
23 |       shimmer: {
24 |         '100%': {
25 |           transform: 'translateX(100%)',
26 |         },
27 |       },
28 |     },
29 |   },
30 |   plugins: [require('@tailwindcss/forms')],
31 | };
32 | export default config;
33 | 


--------------------------------------------------------------------------------
/dashboard/final-example/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2017",
 4 |     "lib": ["dom", "dom.iterable", "esnext"],
 5 |     "allowJs": true,
 6 |     "skipLibCheck": true,
 7 |     "strict": true,
 8 |     "noEmit": true,
 9 |     "esModuleInterop": true,
10 |     "module": "esnext",
11 |     "moduleResolution": "bundler",
12 |     "resolveJsonModule": true,
13 |     "isolatedModules": true,
14 |     "jsx": "preserve",
15 |     "incremental": true,
16 |     "plugins": [
17 |       {
18 |         "name": "next"
19 |       }
20 |     ],
21 |     "baseUrl": ".",
22 |     "paths": {
23 |       "@/*": ["./*"]
24 |     }
25 |   },
26 |   "include": [
27 |     "next-env.d.ts",
28 |     "**/*.ts",
29 |     "**/*.tsx",
30 |     ".next/types/**/*.ts",
31 |     "app/lib/placeholder-data.ts",
32 |     "scripts/seed.js"
33 |   ],
34 |   "exclude": ["node_modules"]
35 | }
36 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/.env.example:
--------------------------------------------------------------------------------
 1 | # Copy from .env.local on the Vercel dashboard
 2 | # https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database
 3 | POSTGRES_URL=
 4 | POSTGRES_PRISMA_URL=
 5 | POSTGRES_URL_NON_POOLING=
 6 | POSTGRES_USER=
 7 | POSTGRES_HOST=
 8 | POSTGRES_PASSWORD=
 9 | POSTGRES_DATABASE=
10 | 
11 | # `openssl rand -base64 32`
12 | AUTH_SECRET=
13 | AUTH_URL=http://localhost:3000/api/auth


--------------------------------------------------------------------------------
/dashboard/starter-example/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | *.pem
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 
27 | # local env files
28 | .env*.local
29 | .env
30 | 
31 | # vercel
32 | .vercel
33 | 
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js App Router Course - Starter
2 | 
3 | This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application.
4 | 
5 | For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.
6 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/layout.tsx:
--------------------------------------------------------------------------------
 1 | export default function RootLayout({
 2 |   children,
 3 | }: {
 4 |   children: React.ReactNode;
 5 | }) {
 6 |   return (
 7 |     <html lang="en">
 8 |       <body>{children}</body>
 9 |     </html>
10 |   );
11 | }
12 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/lib/definitions.ts:
--------------------------------------------------------------------------------
 1 | // This file contains type definitions for your data.
 2 | // It describes the shape of the data, and what data type each property should accept.
 3 | // For simplicity of teaching, we're manually defining these types.
 4 | // However, these types are generated automatically if you're using an ORM such as Prisma.
 5 | export type User = {
 6 |   id: string;
 7 |   name: string;
 8 |   email: string;
 9 |   password: string;
10 | };
11 | 
12 | export type Customer = {
13 |   id: string;
14 |   name: string;
15 |   email: string;
16 |   image_url: string;
17 | };
18 | 
19 | export type Invoice = {
20 |   id: string;
21 |   customer_id: string;
22 |   amount: number;
23 |   date: string;
24 |   // In TypeScript, this is called a string union type.
25 |   // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
26 |   status: 'pending' | 'paid';
27 | };
28 | 
29 | export type Revenue = {
30 |   month: string;
31 |   revenue: number;
32 | };
33 | 
34 | export type LatestInvoice = {
35 |   id: string;
36 |   name: string;
37 |   image_url: string;
38 |   email: string;
39 |   amount: string;
40 | };
41 | 
42 | // The database returns a number for amount, but we later format it to a string with the formatCurrency function
43 | export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & {
44 |   amount: number;
45 | };
46 | 
47 | export type InvoicesTable = {
48 |   id: string;
49 |   customer_id: string;
50 |   name: string;
51 |   email: string;
52 |   image_url: string;
53 |   date: string;
54 |   amount: number;
55 |   status: 'pending' | 'paid';
56 | };
57 | 
58 | export type CustomersTableType = {
59 |   id: string;
60 |   name: string;
61 |   email: string;
62 |   image_url: string;
63 |   total_invoices: number;
64 |   total_pending: number;
65 |   total_paid: number;
66 | };
67 | 
68 | export type FormattedCustomersTable = {
69 |   id: string;
70 |   name: string;
71 |   email: string;
72 |   image_url: string;
73 |   total_invoices: number;
74 |   total_pending: string;
75 |   total_paid: string;
76 | };
77 | 
78 | export type CustomerField = {
79 |   id: string;
80 |   name: string;
81 | };
82 | 
83 | export type InvoiceForm = {
84 |   id: string;
85 |   customer_id: string;
86 |   amount: number;
87 |   status: 'pending' | 'paid';
88 | };
89 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/lib/utils.ts:
--------------------------------------------------------------------------------
 1 | import { Revenue } from './definitions';
 2 | 
 3 | export const formatCurrency = (amount: number) => {
 4 |   return (amount / 100).toLocaleString('en-US', {
 5 |     style: 'currency',
 6 |     currency: 'USD',
 7 |   });
 8 | };
 9 | 
10 | export const formatDateToLocal = (
11 |   dateStr: string,
12 |   locale: string = 'en-US',
13 | ) => {
14 |   const date = new Date(dateStr);
15 |   const options: Intl.DateTimeFormatOptions = {
16 |     day: 'numeric',
17 |     month: 'short',
18 |     year: 'numeric',
19 |   };
20 |   const formatter = new Intl.DateTimeFormat(locale, options);
21 |   return formatter.format(date);
22 | };
23 | 
24 | export const generateYAxis = (revenue: Revenue[]) => {
25 |   // Calculate what labels we need to display on the y-axis
26 |   // based on highest record and in 1000s
27 |   const yAxisLabels = [];
28 |   const highestRecord = Math.max(...revenue.map((month) => month.revenue));
29 |   const topLabel = Math.ceil(highestRecord / 1000) * 1000;
30 | 
31 |   for (let i = topLabel; i >= 0; i -= 1000) {
32 |     yAxisLabels.push(`${i / 1000}K`);
33 |   }
34 | 
35 |   return { yAxisLabels, topLabel };
36 | };
37 | 
38 | export const generatePagination = (currentPage: number, totalPages: number) => {
39 |   // If the total number of pages is 7 or less,
40 |   // display all pages without any ellipsis.
41 |   if (totalPages <= 7) {
42 |     return Array.from({ length: totalPages }, (_, i) => i + 1);
43 |   }
44 | 
45 |   // If the current page is among the first 3 pages,
46 |   // show the first 3, an ellipsis, and the last 2 pages.
47 |   if (currentPage <= 3) {
48 |     return [1, 2, 3, '...', totalPages - 1, totalPages];
49 |   }
50 | 
51 |   // If the current page is among the last 3 pages,
52 |   // show the first 2, an ellipsis, and the last 3 pages.
53 |   if (currentPage >= totalPages - 2) {
54 |     return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
55 |   }
56 | 
57 |   // If the current page is somewhere in the middle,
58 |   // show the first page, an ellipsis, the current page and its neighbors,
59 |   // another ellipsis, and the last page.
60 |   return [
61 |     1,
62 |     '...',
63 |     currentPage - 1,
64 |     currentPage,
65 |     currentPage + 1,
66 |     '...',
67 |     totalPages,
68 |   ];
69 | };
70 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/page.tsx:
--------------------------------------------------------------------------------
 1 | import AcmeLogo from '@/app/ui/acme-logo';
 2 | import { ArrowRightIcon } from '@heroicons/react/24/outline';
 3 | import Link from 'next/link';
 4 | 
 5 | export default function Page() {
 6 |   return (
 7 |     <main className="flex min-h-screen flex-col p-6">
 8 |       <div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
 9 |         {/* <AcmeLogo /> */}
10 |       </div>
11 |       <div className="mt-4 flex grow flex-col gap-4 md:flex-row">
12 |         <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">
13 |           <p className={`text-xl text-gray-800 md:text-3xl md:leading-normal`}>
14 |             <strong>Welcome to Acme.</strong> This is the example for the{' '}
15 |             <a href="https://nextjs.org/learn/" className="text-blue-500">
16 |               Next.js Learn Course
17 |             </a>
18 |             , brought to you by Vercel.
19 |           </p>
20 |           <Link
21 |             href="/login"
22 |             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"
23 |           >
24 |             <span>Log in</span> <ArrowRightIcon className="w-5 md:w-6" />
25 |           </Link>
26 |         </div>
27 |         <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
28 |           {/* Add Hero Images Here */}
29 |         </div>
30 |       </div>
31 |     </main>
32 |   );
33 | }
34 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/query/route.ts:
--------------------------------------------------------------------------------
 1 | // import postgres from 'postgres';
 2 | 
 3 | // const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 4 | 
 5 | // async function listInvoices() {
 6 | // 	const data = await sql`
 7 | //     SELECT invoices.amount, customers.name
 8 | //     FROM invoices
 9 | //     JOIN customers ON invoices.customer_id = customers.id
10 | //     WHERE invoices.amount = 666;
11 | //   `;
12 | 
13 | // 	return data;
14 | // }
15 | 
16 | export async function GET() {
17 |   return Response.json({
18 |     message:
19 |       'Uncomment this file and remove this line. You can delete this file when you are finished.',
20 |   });
21 |   // try {
22 |   // 	return Response.json(await listInvoices());
23 |   // } catch (error) {
24 |   // 	return Response.json({ error }, { status: 500 });
25 |   // }
26 | }
27 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/acme-logo.tsx:
--------------------------------------------------------------------------------
 1 | import { GlobeAltIcon } from '@heroicons/react/24/outline';
 2 | import { lusitana } from '@/app/ui/fonts';
 3 | 
 4 | export default function AcmeLogo() {
 5 |   return (
 6 |     <div
 7 |       className={`${lusitana.className} flex flex-row items-center leading-none text-white`}
 8 |     >
 9 |       <GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
10 |       <p className="text-[44px]">Acme</p>
11 |     </div>
12 |   );
13 | }
14 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/button.tsx:
--------------------------------------------------------------------------------
 1 | import clsx from 'clsx';
 2 | 
 3 | interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
 4 |   children: React.ReactNode;
 5 | }
 6 | 
 7 | export function Button({ children, className, ...rest }: ButtonProps) {
 8 |   return (
 9 |     <button
10 |       {...rest}
11 |       className={clsx(
12 |         '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',
13 |         className,
14 |       )}
15 |     >
16 |       {children}
17 |     </button>
18 |   );
19 | }
20 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/dashboard/cards.tsx:
--------------------------------------------------------------------------------
 1 | import {
 2 |   BanknotesIcon,
 3 |   ClockIcon,
 4 |   UserGroupIcon,
 5 |   InboxIcon,
 6 | } from '@heroicons/react/24/outline';
 7 | import { lusitana } from '@/app/ui/fonts';
 8 | 
 9 | const iconMap = {
10 |   collected: BanknotesIcon,
11 |   customers: UserGroupIcon,
12 |   pending: ClockIcon,
13 |   invoices: InboxIcon,
14 | };
15 | 
16 | export default async function CardWrapper() {
17 |   return (
18 |     <>
19 |       {/* NOTE: Uncomment this code in Chapter 9 */}
20 | 
21 |       {/* <Card title="Collected" value={totalPaidInvoices} type="collected" />
22 |       <Card title="Pending" value={totalPendingInvoices} type="pending" />
23 |       <Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
24 |       <Card
25 |         title="Total Customers"
26 |         value={numberOfCustomers}
27 |         type="customers"
28 |       /> */}
29 |     </>
30 |   );
31 | }
32 | 
33 | export function Card({
34 |   title,
35 |   value,
36 |   type,
37 | }: {
38 |   title: string;
39 |   value: number | string;
40 |   type: 'invoices' | 'customers' | 'pending' | 'collected';
41 | }) {
42 |   const Icon = iconMap[type];
43 | 
44 |   return (
45 |     <div className="rounded-xl bg-gray-50 p-2 shadow-sm">
46 |       <div className="flex p-4">
47 |         {Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null}
48 |         <h3 className="ml-2 text-sm font-medium">{title}</h3>
49 |       </div>
50 |       <p
51 |         className={`${lusitana.className}
52 |           truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
53 |       >
54 |         {value}
55 |       </p>
56 |     </div>
57 |   );
58 | }
59 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/dashboard/latest-invoices.tsx:
--------------------------------------------------------------------------------
 1 | import { ArrowPathIcon } from '@heroicons/react/24/outline';
 2 | import clsx from 'clsx';
 3 | import Image from 'next/image';
 4 | import { lusitana } from '@/app/ui/fonts';
 5 | import { LatestInvoice } from '@/app/lib/definitions';
 6 | export default async function LatestInvoices({
 7 |   latestInvoices,
 8 | }: {
 9 |   latestInvoices: LatestInvoice[];
10 | }) {
11 |   return (
12 |     <div className="flex w-full flex-col md:col-span-4">
13 |       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
14 |         Latest Invoices
15 |       </h2>
16 |       <div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
17 |         {/* NOTE: Uncomment this code in Chapter 7 */}
18 | 
19 |         {/* <div className="bg-white px-6">
20 |           {latestInvoices.map((invoice, i) => {
21 |             return (
22 |               <div
23 |                 key={invoice.id}
24 |                 className={clsx(
25 |                   'flex flex-row items-center justify-between py-4',
26 |                   {
27 |                     'border-t': i !== 0,
28 |                   },
29 |                 )}
30 |               >
31 |                 <div className="flex items-center">
32 |                   <Image
33 |                     src={invoice.image_url}
34 |                     alt={`${invoice.name}'s profile picture`}
35 |                     className="mr-4 rounded-full"
36 |                     width={32}
37 |                     height={32}
38 |                   />
39 |                   <div className="min-w-0">
40 |                     <p className="truncate text-sm font-semibold md:text-base">
41 |                       {invoice.name}
42 |                     </p>
43 |                     <p className="hidden text-sm text-gray-500 sm:block">
44 |                       {invoice.email}
45 |                     </p>
46 |                   </div>
47 |                 </div>
48 |                 <p
49 |                   className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
50 |                 >
51 |                   {invoice.amount}
52 |                 </p>
53 |               </div>
54 |             );
55 |           })}
56 |         </div> */}
57 |         <div className="flex items-center pb-2 pt-6">
58 |           <ArrowPathIcon className="h-5 w-5 text-gray-500" />
59 |           <h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
60 |         </div>
61 |       </div>
62 |     </div>
63 |   );
64 | }
65 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/dashboard/nav-links.tsx:
--------------------------------------------------------------------------------
 1 | import {
 2 |   UserGroupIcon,
 3 |   HomeIcon,
 4 |   DocumentDuplicateIcon,
 5 | } from '@heroicons/react/24/outline';
 6 | 
 7 | // Map of links to display in the side navigation.
 8 | // Depending on the size of the application, this would be stored in a database.
 9 | const links = [
10 |   { name: 'Home', href: '/dashboard', icon: HomeIcon },
11 |   {
12 |     name: 'Invoices',
13 |     href: '/dashboard/invoices',
14 |     icon: DocumentDuplicateIcon,
15 |   },
16 |   { name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
17 | ];
18 | 
19 | export default function NavLinks() {
20 |   return (
21 |     <>
22 |       {links.map((link) => {
23 |         const LinkIcon = link.icon;
24 |         return (
25 |           <a
26 |             key={link.name}
27 |             href={link.href}
28 |             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"
29 |           >
30 |             <LinkIcon className="w-6" />
31 |             <p className="hidden md:block">{link.name}</p>
32 |           </a>
33 |         );
34 |       })}
35 |     </>
36 |   );
37 | }
38 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/dashboard/revenue-chart.tsx:
--------------------------------------------------------------------------------
 1 | import { generateYAxis } from '@/app/lib/utils';
 2 | import { CalendarIcon } from '@heroicons/react/24/outline';
 3 | import { lusitana } from '@/app/ui/fonts';
 4 | import { Revenue } from '@/app/lib/definitions';
 5 | 
 6 | // This component is representational only.
 7 | // For data visualization UI, check out:
 8 | // https://www.tremor.so/
 9 | // https://www.chartjs.org/
10 | // https://airbnb.io/visx/
11 | 
12 | export default async function RevenueChart({
13 |   revenue,
14 | }: {
15 |   revenue: Revenue[];
16 | }) {
17 |   const chartHeight = 350;
18 |   // NOTE: Uncomment this code in Chapter 7
19 | 
20 |   // const { yAxisLabels, topLabel } = generateYAxis(revenue);
21 | 
22 |   // if (!revenue || revenue.length === 0) {
23 |   //   return <p className="mt-4 text-gray-400">No data available.</p>;
24 |   // }
25 | 
26 |   return (
27 |     <div className="w-full md:col-span-4">
28 |       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
29 |         Recent Revenue
30 |       </h2>
31 |       {/* NOTE: Uncomment this code in Chapter 7 */}
32 | 
33 |       {/* <div className="rounded-xl bg-gray-50 p-4">
34 |         <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">
35 |           <div
36 |             className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
37 |             style={{ height: `${chartHeight}px` }}
38 |           >
39 |             {yAxisLabels.map((label) => (
40 |               <p key={label}>{label}</p>
41 |             ))}
42 |           </div>
43 | 
44 |           {revenue.map((month) => (
45 |             <div key={month.month} className="flex flex-col items-center gap-2">
46 |               <div
47 |                 className="w-full rounded-md bg-blue-300"
48 |                 style={{
49 |                   height: `${(chartHeight / topLabel) * month.revenue}px`,
50 |                 }}
51 |               ></div>
52 |               <p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
53 |                 {month.month}
54 |               </p>
55 |             </div>
56 |           ))}
57 |         </div>
58 |         <div className="flex items-center pb-2 pt-6">
59 |           <CalendarIcon className="h-5 w-5 text-gray-500" />
60 |           <h3 className="ml-2 text-sm text-gray-500 ">Last 12 months</h3>
61 |         </div>
62 |       </div> */}
63 |     </div>
64 |   );
65 | }
66 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/dashboard/sidenav.tsx:
--------------------------------------------------------------------------------
 1 | import Link from 'next/link';
 2 | import NavLinks from '@/app/ui/dashboard/nav-links';
 3 | import AcmeLogo from '@/app/ui/acme-logo';
 4 | import { PowerIcon } from '@heroicons/react/24/outline';
 5 | 
 6 | export default function SideNav() {
 7 |   return (
 8 |     <div className="flex h-full flex-col px-3 py-4 md:px-2">
 9 |       <Link
10 |         className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
11 |         href="/"
12 |       >
13 |         <div className="w-32 text-white md:w-40">
14 |           <AcmeLogo />
15 |         </div>
16 |       </Link>
17 |       <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
18 |         <NavLinks />
19 |         <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
20 |         <form>
21 |           <button className="flex h-[48px] w-full 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">
22 |             <PowerIcon className="w-6" />
23 |             <div className="hidden md:block">Sign Out</div>
24 |           </button>
25 |         </form>
26 |       </div>
27 |     </div>
28 |   );
29 | }
30 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/global.css:
--------------------------------------------------------------------------------
 1 | @tailwind base;
 2 | @tailwind components;
 3 | @tailwind utilities;
 4 | 
 5 | input[type='number'] {
 6 |   -moz-appearance: textfield;
 7 |   appearance: textfield;
 8 | }
 9 | 
10 | input[type='number']::-webkit-inner-spin-button {
11 |   -webkit-appearance: none;
12 |   margin: 0;
13 | }
14 | 
15 | input[type='number']::-webkit-outer-spin-button {
16 |   -webkit-appearance: none;
17 |   margin: 0;
18 | }
19 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/invoices/breadcrumbs.tsx:
--------------------------------------------------------------------------------
 1 | import { clsx } from 'clsx';
 2 | import Link from 'next/link';
 3 | import { lusitana } from '@/app/ui/fonts';
 4 | 
 5 | interface Breadcrumb {
 6 |   label: string;
 7 |   href: string;
 8 |   active?: boolean;
 9 | }
10 | 
11 | export default function Breadcrumbs({
12 |   breadcrumbs,
13 | }: {
14 |   breadcrumbs: Breadcrumb[];
15 | }) {
16 |   return (
17 |     <nav aria-label="Breadcrumb" className="mb-6 block">
18 |       <ol className={clsx(lusitana.className, 'flex text-xl md:text-2xl')}>
19 |         {breadcrumbs.map((breadcrumb, index) => (
20 |           <li
21 |             key={breadcrumb.href}
22 |             aria-current={breadcrumb.active}
23 |             className={clsx(
24 |               breadcrumb.active ? 'text-gray-900' : 'text-gray-500',
25 |             )}
26 |           >
27 |             <Link href={breadcrumb.href}>{breadcrumb.label}</Link>
28 |             {index < breadcrumbs.length - 1 ? (
29 |               <span className="mx-3 inline-block">/</span>
30 |             ) : null}
31 |           </li>
32 |         ))}
33 |       </ol>
34 |     </nav>
35 |   );
36 | }
37 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/invoices/buttons.tsx:
--------------------------------------------------------------------------------
 1 | import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
 2 | import Link from 'next/link';
 3 | 
 4 | export function CreateInvoice() {
 5 |   return (
 6 |     <Link
 7 |       href="/dashboard/invoices/create"
 8 |       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"
 9 |     >
10 |       <span className="hidden md:block">Create Invoice</span>{' '}
11 |       <PlusIcon className="h-5 md:ml-4" />
12 |     </Link>
13 |   );
14 | }
15 | 
16 | export function UpdateInvoice({ id }: { id: string }) {
17 |   return (
18 |     <Link
19 |       href="/dashboard/invoices"
20 |       className="rounded-md border p-2 hover:bg-gray-100"
21 |     >
22 |       <PencilIcon className="w-5" />
23 |     </Link>
24 |   );
25 | }
26 | 
27 | export function DeleteInvoice({ id }: { id: string }) {
28 |   return (
29 |     <>
30 |       <button type="submit" className="rounded-md border p-2 hover:bg-gray-100">
31 |         <span className="sr-only">Delete</span>
32 |         <TrashIcon className="w-5" />
33 |       </button>
34 |     </>
35 |   );
36 | }
37 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/invoices/status.tsx:
--------------------------------------------------------------------------------
 1 | import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
 2 | import clsx from 'clsx';
 3 | 
 4 | export default function InvoiceStatus({ status }: { status: string }) {
 5 |   return (
 6 |     <span
 7 |       className={clsx(
 8 |         'inline-flex items-center rounded-full px-2 py-1 text-xs',
 9 |         {
10 |           'bg-gray-100 text-gray-500': status === 'pending',
11 |           'bg-green-500 text-white': status === 'paid',
12 |         },
13 |       )}
14 |     >
15 |       {status === 'pending' ? (
16 |         <>
17 |           Pending
18 |           <ClockIcon className="ml-1 w-4 text-gray-500" />
19 |         </>
20 |       ) : null}
21 |       {status === 'paid' ? (
22 |         <>
23 |           Paid
24 |           <CheckIcon className="ml-1 w-4 text-white" />
25 |         </>
26 |       ) : null}
27 |     </span>
28 |   );
29 | }
30 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/login-form.tsx:
--------------------------------------------------------------------------------
 1 | import { lusitana } from '@/app/ui/fonts';
 2 | import {
 3 |   AtSymbolIcon,
 4 |   KeyIcon,
 5 |   ExclamationCircleIcon,
 6 | } from '@heroicons/react/24/outline';
 7 | import { ArrowRightIcon } from '@heroicons/react/20/solid';
 8 | import { Button } from './button';
 9 | 
10 | export default function LoginForm() {
11 |   return (
12 |     <form className="space-y-3">
13 |       <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
14 |         <h1 className={`${lusitana.className} mb-3 text-2xl`}>
15 |           Please log in to continue.
16 |         </h1>
17 |         <div className="w-full">
18 |           <div>
19 |             <label
20 |               className="mb-3 mt-5 block text-xs font-medium text-gray-900"
21 |               htmlFor="email"
22 |             >
23 |               Email
24 |             </label>
25 |             <div className="relative">
26 |               <input
27 |                 className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
28 |                 id="email"
29 |                 type="email"
30 |                 name="email"
31 |                 placeholder="Enter your email address"
32 |                 required
33 |               />
34 |               <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" />
35 |             </div>
36 |           </div>
37 |           <div className="mt-4">
38 |             <label
39 |               className="mb-3 mt-5 block text-xs font-medium text-gray-900"
40 |               htmlFor="password"
41 |             >
42 |               Password
43 |             </label>
44 |             <div className="relative">
45 |               <input
46 |                 className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
47 |                 id="password"
48 |                 type="password"
49 |                 name="password"
50 |                 placeholder="Enter password"
51 |                 required
52 |                 minLength={6}
53 |               />
54 |               <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" />
55 |             </div>
56 |           </div>
57 |         </div>
58 |         <Button className="mt-4 w-full">
59 |           Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
60 |         </Button>
61 |         <div className="flex h-8 items-end space-x-1">
62 |           {/* Add form errors here */}
63 |         </div>
64 |       </div>
65 |     </form>
66 |   );
67 | }
68 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/app/ui/search.tsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
 4 | 
 5 | export default function Search({ placeholder }: { placeholder: string }) {
 6 |   return (
 7 |     <div className="relative flex flex-1 flex-shrink-0">
 8 |       <label htmlFor="search" className="sr-only">
 9 |         Search
10 |       </label>
11 |       <input
12 |         className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
13 |         placeholder={placeholder}
14 |       />
15 |       <MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
16 |     </div>
17 |   );
18 | }
19 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 | 
3 | const nextConfig: NextConfig = {
4 |   /* config options here */
5 | };
6 | 
7 | export default nextConfig;
8 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "@heroicons/react": "^2.2.0",
10 |     "@tailwindcss/forms": "^0.5.10",
11 |     "autoprefixer": "10.4.20",
12 |     "bcrypt": "^5.1.1",
13 |     "clsx": "^2.1.1",
14 |     "next": "latest",
15 |     "next-auth": "5.0.0-beta.25",
16 |     "postcss": "8.5.1",
17 |     "postgres": "^3.4.6",
18 |     "react": "latest",
19 |     "react-dom": "latest",
20 |     "tailwindcss": "3.4.17",
21 |     "typescript": "5.7.3",
22 |     "use-debounce": "^10.0.4",
23 |     "zod": "^3.25.17"
24 |   },
25 |   "devDependencies": {
26 |     "@types/bcrypt": "^5.0.2",
27 |     "@types/node": "22.10.7",
28 |     "@types/react": "19.0.7",
29 |     "@types/react-dom": "19.0.3"
30 |   },
31 |   "pnpm": {
32 |     "onlyBuiltDependencies": [
33 |       "bcrypt",
34 |       "sharp"
35 |     ]
36 |   }
37 | }
38 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | };
7 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/amy-burns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/amy-burns.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/balazs-orban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/balazs-orban.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/delba-de-oliveira.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/delba-de-oliveira.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/evil-rabbit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/evil-rabbit.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/lee-robinson.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/lee-robinson.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/customers/michael-novotny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/customers/michael-novotny.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/favicon.ico


--------------------------------------------------------------------------------
/dashboard/starter-example/public/hero-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/hero-desktop.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/hero-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/hero-mobile.png


--------------------------------------------------------------------------------
/dashboard/starter-example/public/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/dashboard/starter-example/public/opengraph-image.png


--------------------------------------------------------------------------------
/dashboard/starter-example/tailwind.config.ts:
--------------------------------------------------------------------------------
 1 | import type { Config } from 'tailwindcss';
 2 | 
 3 | const config: Config = {
 4 |   content: [
 5 |     './pages/**/*.{js,ts,jsx,tsx,mdx}',
 6 |     './components/**/*.{js,ts,jsx,tsx,mdx}',
 7 |     './app/**/*.{js,ts,jsx,tsx,mdx}',
 8 |   ],
 9 |   theme: {
10 |     extend: {
11 |       gridTemplateColumns: {
12 |         '13': 'repeat(13, minmax(0, 1fr))',
13 |       },
14 |       colors: {
15 |         blue: {
16 |           400: '#2589FE',
17 |           500: '#0070F3',
18 |           600: '#2F6FEB',
19 |         },
20 |       },
21 |     },
22 |     keyframes: {
23 |       shimmer: {
24 |         '100%': {
25 |           transform: 'translateX(100%)',
26 |         },
27 |       },
28 |     },
29 |   },
30 |   plugins: [require('@tailwindcss/forms')],
31 | };
32 | export default config;
33 | 


--------------------------------------------------------------------------------
/dashboard/starter-example/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2017",
 4 |     "lib": ["dom", "dom.iterable", "esnext"],
 5 |     "allowJs": true,
 6 |     "skipLibCheck": true,
 7 |     "strict": true,
 8 |     "noEmit": true,
 9 |     "esModuleInterop": true,
10 |     "module": "esnext",
11 |     "moduleResolution": "bundler",
12 |     "resolveJsonModule": true,
13 |     "isolatedModules": true,
14 |     "jsx": "preserve",
15 |     "incremental": true,
16 |     "plugins": [
17 |       {
18 |         "name": "next"
19 |       }
20 |     ],
21 |     "baseUrl": ".",
22 |     "paths": {
23 |       "@/*": ["./*"]
24 |     }
25 |   },
26 |   "include": [
27 |     "next-env.d.ts",
28 |     "**/*.ts",
29 |     "**/*.tsx",
30 |     ".next/types/**/*.ts",
31 |     "app/lib/placeholder-data.ts",
32 |     "scripts/seed.js"
33 |   ],
34 |   "exclude": ["node_modules"]
35 | }
36 | 


--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2024 Vercel, Inc.
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
 5 |     "prettier": "prettier --write --ignore-unknown .",
 6 |     "prettier:check": "prettier --check --ignore-unknown .",
 7 |     "start": "next start",
 8 |     "test": "npm run lint && npm run prettier:check"
 9 |   },
10 |   "dependencies": {
11 |     "@tailwindcss/forms": "^0.5.7",
12 |     "next": "^14.0.0"
13 |   },
14 |   "devDependencies": {
15 |     "@vercel/style-guide": "^5.0.1",
16 |     "eslint": "8.52.0",
17 |     "eslint-config-next": "14.0.0",
18 |     "eslint-config-prettier": "9.0.0",
19 |     "prettier": "3.0.3",
20 |     "prettier-plugin-tailwindcss": "0.5.4"
21 |   },
22 |   "packageManager": "pnpm@8.7.0",
23 |   "engines": {
24 |     "node": ">=18.17.0"
25 |   }
26 | }
27 | 


--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | const styleguide = require('@vercel/style-guide/prettier');
2 | 
3 | module.exports = {
4 |   ...styleguide,
5 |   plugins: [...styleguide.plugins, 'prettier-plugin-tailwindcss'],
6 | };
7 | 


--------------------------------------------------------------------------------
/seo/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | *.pem
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 | 
33 | # vercel
34 | .vercel
35 | 


--------------------------------------------------------------------------------
/seo/README.md:
--------------------------------------------------------------------------------
1 | # next-seo-starter
2 | 
3 | This repository contains the starter template for the [Improving Web Vitals](https://nextjs.org/learn/seo/improve/lighthouse) example of the Next.js [learn SEO course](https://nextjs.org/learn/seo/introduction-to-seo).
4 | 


--------------------------------------------------------------------------------
/seo/components/CodeSampleModal.js:
--------------------------------------------------------------------------------
 1 | import Modal from 'react-modal';
 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
 3 | import { twilight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 4 | 
 5 | const customStyles = {
 6 |   content: {
 7 |     top: '50%',
 8 |     left: '50%',
 9 |     right: 'auto',
10 |     bottom: 'auto',
11 |     marginRight: '-50%',
12 |     transform: 'translate(-50%, -50%)',
13 |     maxWidth: '100%',
14 |   },
15 | };
16 | 
17 | Modal.setAppElement('#__next');
18 | 
19 | export default function CodeSampleModal({ isOpen, closeModal }) {
20 |   return (
21 |     <Modal
22 |       isOpen={isOpen}
23 |       onRequestClose={closeModal}
24 |       style={customStyles}
25 |       contentLabel="Code Sample"
26 |     >
27 |       <p>Wonder no more!</p>
28 |       <SyntaxHighlighter language="javascript" style={twilight}>
29 |         {`function printHelloWorld() { \n  console.log('Hello World!'); \n}`}
30 |       </SyntaxHighlighter>
31 |       <button onClick={closeModal}>Close</button>
32 |     </Modal>
33 |   );
34 | }
35 | 


--------------------------------------------------------------------------------
/seo/countries.js:
--------------------------------------------------------------------------------
 1 | export const countries = [
 2 |   { name: 'China', cca2: 'CN', population: 1439323776 },
 3 |   { name: 'India', cca2: 'IN', population: 1380004385 },
 4 |   { name: 'United States', cca2: 'US', population: 331002651 },
 5 |   { name: 'Indonesia', cca2: 'ID', population: 273523615 },
 6 |   { name: 'Pakistan', cca2: 'PK', population: 220892340 },
 7 |   { name: 'Brazil', cca2: 'BR', population: 212559417 },
 8 |   { name: 'Nigeria', cca2: 'NG', population: 206139589 },
 9 |   { name: 'Bangladesh', cca2: 'BD', population: 164689383 },
10 |   { name: 'Russia', cca2: 'RU', population: 145934462 },
11 |   { name: 'Mexico', cca2: 'MX', population: 128932753 },
12 |   { name: 'Japan', cca2: 'JP', population: 126476461 },
13 |   { name: 'Philippines', cca2: 'PH', population: 109581078 },
14 |   { name: 'Egypt', cca2: 'EG', population: 102334404 },
15 |   { name: 'Ethiopia', cca2: 'ET', population: 114963588 },
16 |   { name: 'Vietnam', cca2: 'VN', population: 97338579 },
17 |   { name: 'Germany', cca2: 'DE', population: 83783942 },
18 |   { name: 'Turkey', cca2: 'TR', population: 84339067 },
19 |   { name: 'Iran', cca2: 'IR', population: 83992949 },
20 |   { name: 'Thailand', cca2: 'TH', population: 69799978 },
21 |   { name: 'United Kingdom', cca2: 'GB', population: 67886011 },
22 |   { name: 'France', cca2: 'FR', population: 65273511 },
23 |   { name: 'Italy', cca2: 'IT', population: 60461826 },
24 |   { name: 'South Africa', cca2: 'ZA', population: 59308690 },
25 |   { name: 'Tanzania', cca2: 'TZ', population: 59734218 },
26 |   { name: 'Myanmar', cca2: 'MM', population: 54409800 },
27 |   { name: 'South Korea', cca2: 'KR', population: 51269185 },
28 |   { name: 'Colombia', cca2: 'CO', population: 50882891 },
29 |   { name: 'Kenya', cca2: 'KE', population: 53771296 },
30 |   { name: 'Spain', cca2: 'ES', population: 46754778 },
31 |   { name: 'Argentina', cca2: 'AR', population: 45195774 },
32 | ];
33 | 


--------------------------------------------------------------------------------
/seo/demo/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | *.pem
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 | 
33 | # vercel
34 | .vercel
35 | 


--------------------------------------------------------------------------------
/seo/demo/README.md:
--------------------------------------------------------------------------------
1 | This is the final demo for the [Learn SEO](https://nextjs.org/learn/seo/introduction-to-seo) example.
2 | 


--------------------------------------------------------------------------------
/seo/demo/components/CodeSampleModal.js:
--------------------------------------------------------------------------------
 1 | import Modal from 'react-modal';
 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
 3 | import { twilight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 4 | 
 5 | const customStyles = {
 6 |   content: {
 7 |     top: '50%',
 8 |     left: '50%',
 9 |     right: 'auto',
10 |     bottom: 'auto',
11 |     marginRight: '-50%',
12 |     transform: 'translate(-50%, -50%)',
13 |     maxWidth: '100%',
14 |   },
15 | };
16 | 
17 | Modal.setAppElement('#__next');
18 | 
19 | export default function CodeSampleModal({ isOpen, closeModal }) {
20 |   return (
21 |     <Modal
22 |       isOpen={isOpen}
23 |       onRequestClose={closeModal}
24 |       style={customStyles}
25 |       contentLabel="Code Sample"
26 |     >
27 |       <p>Wonder no more!</p>
28 |       <SyntaxHighlighter language="javascript" style={twilight}>
29 |         {`function printHelloWorld() { \n  console.log('Hello World!'); \n}`}
30 |       </SyntaxHighlighter>
31 |       <button onClick={closeModal}>Close</button>
32 |     </Modal>
33 |   );
34 | }
35 | 


--------------------------------------------------------------------------------
/seo/demo/countries.js:
--------------------------------------------------------------------------------
 1 | export const countries = [
 2 |   { name: 'China', cca2: 'CN', population: 1439323776 },
 3 |   { name: 'India', cca2: 'IN', population: 1380004385 },
 4 |   { name: 'United States', cca2: 'US', population: 331002651 },
 5 |   { name: 'Indonesia', cca2: 'ID', population: 273523615 },
 6 |   { name: 'Pakistan', cca2: 'PK', population: 220892340 },
 7 |   { name: 'Brazil', cca2: 'BR', population: 212559417 },
 8 |   { name: 'Nigeria', cca2: 'NG', population: 206139589 },
 9 |   { name: 'Bangladesh', cca2: 'BD', population: 164689383 },
10 |   { name: 'Russia', cca2: 'RU', population: 145934462 },
11 |   { name: 'Mexico', cca2: 'MX', population: 128932753 },
12 |   { name: 'Japan', cca2: 'JP', population: 126476461 },
13 |   { name: 'Philippines', cca2: 'PH', population: 109581078 },
14 |   { name: 'Egypt', cca2: 'EG', population: 102334404 },
15 |   { name: 'Ethiopia', cca2: 'ET', population: 114963588 },
16 |   { name: 'Vietnam', cca2: 'VN', population: 97338579 },
17 |   { name: 'Germany', cca2: 'DE', population: 83783942 },
18 |   { name: 'Turkey', cca2: 'TR', population: 84339067 },
19 |   { name: 'Iran', cca2: 'IR', population: 83992949 },
20 |   { name: 'Thailand', cca2: 'TH', population: 69799978 },
21 |   { name: 'United Kingdom', cca2: 'GB', population: 67886011 },
22 |   { name: 'France', cca2: 'FR', population: 65273511 },
23 |   { name: 'Italy', cca2: 'IT', population: 60461826 },
24 |   { name: 'South Africa', cca2: 'ZA', population: 59308690 },
25 |   { name: 'Tanzania', cca2: 'TZ', population: 59734218 },
26 |   { name: 'Myanmar', cca2: 'MM', population: 54409800 },
27 |   { name: 'South Korea', cca2: 'KR', population: 51269185 },
28 |   { name: 'Colombia', cca2: 'CO', population: 50882891 },
29 |   { name: 'Kenya', cca2: 'KE', population: 53771296 },
30 |   { name: 'Spain', cca2: 'ES', population: 46754778 },
31 |   { name: 'Argentina', cca2: 'AR', population: 45195774 },
32 | ];
33 | 


--------------------------------------------------------------------------------
/seo/demo/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "fuse.js": "^6.6.2",
10 |     "lodash": "^4.17.21",
11 |     "next": "^15.1.6",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "react-modal": "^3.16.3",
15 |     "react-syntax-highlighter": "^15.6.1"
16 |   },
17 |   "engines": {
18 |     "node": ">=18"
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/seo/demo/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function MyApp({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/seo/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/seo/demo/public/favicon.ico


--------------------------------------------------------------------------------
/seo/demo/public/large-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/seo/demo/public/large-image.jpg


--------------------------------------------------------------------------------
/seo/demo/public/vercel.svg:
--------------------------------------------------------------------------------
1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2 |     xmlns="http://www.w3.org/2000/svg">
3 |     <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4 | </svg>


--------------------------------------------------------------------------------
/seo/demo/styles/Home.module.css:
--------------------------------------------------------------------------------
  1 | .container {
  2 |   min-height: 100vh;
  3 |   padding: 1rem 0.5rem 0;
  4 | }
  5 | 
  6 | .title a {
  7 |   color: #0070f3;
  8 |   text-decoration: none;
  9 | }
 10 | 
 11 | .title a:hover,
 12 | .title a:focus,
 13 | .title a:active {
 14 |   text-decoration: underline;
 15 | }
 16 | 
 17 | .title {
 18 |   margin: 0 0 1rem;
 19 |   line-height: 1.15;
 20 |   font-size: 3.6rem;
 21 | }
 22 | 
 23 | .title {
 24 |   text-align: center;
 25 | }
 26 | 
 27 | .heroImage {
 28 |   margin-bottom: 1rem;
 29 | }
 30 | 
 31 | .secondaryHeading {
 32 |   margin: 0 0 1rem;
 33 | }
 34 | 
 35 | .input {
 36 |   padding: 0.5rem;
 37 |   width: 300px;
 38 |   margin-bottom: 1rem;
 39 | }
 40 | 
 41 | .countries {
 42 |   display: grid;
 43 |   grid-gap: 1rem;
 44 | }
 45 | 
 46 | .country {
 47 |   border: 1px solid #000;
 48 |   border-radius: 0.25rem;
 49 |   padding: 0.25rem 0.5rem;
 50 | }
 51 | 
 52 | .codeSampleBlock {
 53 |   padding: 3rem 0;
 54 | }
 55 | 
 56 | .codeSampleBlock p {
 57 |   font-size: 1.3rem;
 58 |   margin-bottom: 1rem;
 59 | }
 60 | 
 61 | .footer {
 62 |   width: 100%;
 63 |   height: 100px;
 64 |   border-top: 1px solid #eaeaea;
 65 |   display: flex;
 66 |   justify-content: center;
 67 |   align-items: center;
 68 | }
 69 | 
 70 | .logo {
 71 |   margin-left: 0.5rem;
 72 |   max-width: 72px;
 73 | }
 74 | 
 75 | .footer a {
 76 |   display: flex;
 77 |   justify-content: center;
 78 |   align-items: center;
 79 | }
 80 | 
 81 | @media (min-width: 800px) {
 82 |   .countries {
 83 |     grid-template-columns: 1fr 1fr;
 84 |   }
 85 | }
 86 | 
 87 | @media (min-width: 1024px) {
 88 |   .heroImage {
 89 |     margin: 0 auto 1rem;
 90 |     max-width: 50vw;
 91 |   }
 92 | 
 93 |   .secondaryHeading {
 94 |     text-align: center;
 95 |   }
 96 | 
 97 |   .input {
 98 |     margin: 0 auto 1rem;
 99 |     display: block;
100 |   }
101 | 
102 |   .countries {
103 |     grid-template-columns: 1fr 1fr 1fr;
104 |   }
105 | 
106 |   .codeSampleBlock {
107 |     text-align: center;
108 |   }
109 | }
110 | 


--------------------------------------------------------------------------------
/seo/demo/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     Inter,
 7 |     -apple-system,
 8 |     BlinkMacSystemFont,
 9 |     Segoe UI,
10 |     Roboto,
11 |     Oxygen,
12 |     Ubuntu,
13 |     Cantarell,
14 |     Fira Sans,
15 |     Droid Sans,
16 |     Helvetica Neue,
17 |     sans-serif;
18 | }
19 | 
20 | a {
21 |   color: inherit;
22 |   text-decoration: none;
23 | }
24 | 
25 | * {
26 |   box-sizing: border-box;
27 | }
28 | 
29 | img {
30 |   max-width: 100%;
31 |   height: auto;
32 | }
33 | 
34 | h1,
35 | h2,
36 | p,
37 | ul {
38 |   margin: 0;
39 | }
40 | 
41 | ul {
42 |   padding: 0;
43 |   list-style: none;
44 | }
45 | 
46 | button {
47 |   padding: 0.5rem 1rem;
48 |   font-weight: bold;
49 | }
50 | 


--------------------------------------------------------------------------------
/seo/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "private": true,
 3 |   "scripts": {
 4 |     "build": "next build",
 5 |     "dev": "next dev --turbopack",
 6 |     "start": "next start"
 7 |   },
 8 |   "dependencies": {
 9 |     "fuse.js": "^6.6.2",
10 |     "lodash": "^4.17.21",
11 |     "next": "^15.1.6",
12 |     "react": "latest",
13 |     "react-dom": "latest",
14 |     "react-modal": "^3.16.3",
15 |     "react-syntax-highlighter": "^15.6.1"
16 |   },
17 |   "engines": {
18 |     "node": ">=18"
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/seo/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/global.css';
2 | 
3 | export default function MyApp({ Component, pageProps }) {
4 |   return <Component {...pageProps} />;
5 | }
6 | 


--------------------------------------------------------------------------------
/seo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/seo/public/favicon.ico


--------------------------------------------------------------------------------
/seo/public/large-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel/next-learn/1041eeac0ec343d34e0c53e2fb407a68ea87c655/seo/public/large-image.jpg


--------------------------------------------------------------------------------
/seo/public/vercel.svg:
--------------------------------------------------------------------------------
1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2 |     xmlns="http://www.w3.org/2000/svg">
3 |     <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4 | </svg>


--------------------------------------------------------------------------------
/seo/styles/Home.module.css:
--------------------------------------------------------------------------------
  1 | .container {
  2 |   min-height: 100vh;
  3 |   padding: 1rem 0.5rem 0;
  4 | }
  5 | 
  6 | .title a {
  7 |   color: #0070f3;
  8 |   text-decoration: none;
  9 | }
 10 | 
 11 | .title a:hover,
 12 | .title a:focus,
 13 | .title a:active {
 14 |   text-decoration: underline;
 15 | }
 16 | 
 17 | .title {
 18 |   margin: 0 0 1rem;
 19 |   line-height: 1.15;
 20 |   font-size: 3.6rem;
 21 | }
 22 | 
 23 | .title {
 24 |   text-align: center;
 25 | }
 26 | 
 27 | .heroImage {
 28 |   margin-bottom: 1rem;
 29 | }
 30 | 
 31 | .secondaryHeading {
 32 |   margin: 0 0 1rem;
 33 | }
 34 | 
 35 | .input {
 36 |   padding: 0.5rem;
 37 |   width: 300px;
 38 |   margin-bottom: 1rem;
 39 | }
 40 | 
 41 | .countries {
 42 |   display: grid;
 43 |   grid-gap: 1rem;
 44 | }
 45 | 
 46 | .country {
 47 |   border: 1px solid #000;
 48 |   border-radius: 0.25rem;
 49 |   padding: 0.25rem 0.5rem;
 50 | }
 51 | 
 52 | .codeSampleBlock {
 53 |   padding: 3rem 0;
 54 | }
 55 | 
 56 | .codeSampleBlock p {
 57 |   font-size: 1.3rem;
 58 |   margin-bottom: 1rem;
 59 | }
 60 | 
 61 | .footer {
 62 |   width: 100%;
 63 |   height: 100px;
 64 |   border-top: 1px solid #eaeaea;
 65 |   display: flex;
 66 |   justify-content: center;
 67 |   align-items: center;
 68 | }
 69 | 
 70 | .logo {
 71 |   margin-left: 0.5rem;
 72 |   max-width: 72px;
 73 | }
 74 | 
 75 | .footer a {
 76 |   display: flex;
 77 |   justify-content: center;
 78 |   align-items: center;
 79 | }
 80 | 
 81 | @media (min-width: 800px) {
 82 |   .countries {
 83 |     grid-template-columns: 1fr 1fr;
 84 |   }
 85 | }
 86 | 
 87 | @media (min-width: 1024px) {
 88 |   .heroImage {
 89 |     margin: 0 auto 1rem;
 90 |     max-width: 50vw;
 91 |   }
 92 | 
 93 |   .secondaryHeading {
 94 |     text-align: center;
 95 |   }
 96 | 
 97 |   .input {
 98 |     margin: 0 auto 1rem;
 99 |     display: block;
100 |   }
101 | 
102 |   .countries {
103 |     grid-template-columns: 1fr 1fr 1fr;
104 |   }
105 | 
106 |   .codeSampleBlock {
107 |     text-align: center;
108 |   }
109 | }
110 | 


--------------------------------------------------------------------------------
/seo/styles/global.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   padding: 0;
 4 |   margin: 0;
 5 |   font-family:
 6 |     Inter,
 7 |     -apple-system,
 8 |     BlinkMacSystemFont,
 9 |     Segoe UI,
10 |     Roboto,
11 |     Oxygen,
12 |     Ubuntu,
13 |     Cantarell,
14 |     Fira Sans,
15 |     Droid Sans,
16 |     Helvetica Neue,
17 |     sans-serif;
18 | }
19 | 
20 | a {
21 |   color: inherit;
22 |   text-decoration: none;
23 | }
24 | 
25 | * {
26 |   box-sizing: border-box;
27 | }
28 | 
29 | img {
30 |   max-width: 100%;
31 |   height: auto;
32 | }
33 | 
34 | h1,
35 | h2,
36 | p,
37 | ul {
38 |   margin: 0;
39 | }
40 | 
41 | ul {
42 |   padding: 0;
43 |   list-style: none;
44 | }
45 | 
46 | button {
47 |   padding: 0.5rem 1rem;
48 |   font-weight: bold;
49 | }
50 | 


--------------------------------------------------------------------------------