├── .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 | --------------------------------------------------------------------------------