├── .github └── workflows │ └── next.yaml ├── .gitignore ├── README.md ├── components ├── corner.js ├── footer.js ├── github.js ├── header.js ├── layout.js ├── linkedin.js └── twitter.js ├── context ├── courseInfoContext.js └── headerContext.js ├── course.json ├── data ├── course.js └── lesson.js ├── lessons ├── 01-the-first-section │ └── A-intro.md ├── 02-a-second-section │ ├── A-first-lesson.md │ ├── B-second-lesson.md │ └── meta.json └── 03-thoughts-on-js-and-css │ ├── A-css.md │ ├── B-javascript.md │ ├── C-npm.md │ └── meta.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── index.js └── lessons │ └── [section] │ └── [slug].js ├── public ├── .nojekyll └── images │ ├── BRAND-WHearts.png │ ├── author.jpg │ ├── course-icon.png │ └── social-share-cover.jpg └── styles ├── courses.css ├── footer.css └── variables.css /.github/workflows/next.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy NextJS Course Site to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: npm install, export 14 | run: | 15 | npm install 16 | npm run export 17 | - name: Deploy site to gh-pages branch 18 | uses: crazy-max/ghaction-github-pages@v2 19 | with: 20 | target_branch: gh-pages 21 | build_dir: out 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

next-course-starter


2 | 3 |

4 | A NextJS starter to get you started creating educational materials using Markdown 5 |

6 | 7 | ## Get Started 8 | 9 | 1. Set up Node.js v14+ 10 | 1. Clone this repo 11 | 1. Run `npm install` 12 | 1. Run `npm run dev` to start the dev server 13 | 1. Open http://localhost:3000 in a browser 14 | 15 | ## Configure Your Course 16 | 17 | There are several things to configure before getting started. 18 | 19 | ### course.json 20 | 21 | This json file allows you to configure the details of the site. Update the info here and it'll update it everywhere throughout the course website. 22 | 23 | - _author.name_ – Your name 24 | - _author.company_ – The company you work at or whatever you want as your subtitle. Optional. 25 | - _title_ – The title of your course 26 | - _subtitle_ – The subtitle of your course. Optional. 27 | - _frontendMastersLink_ – A link to the published video on FrontendMasters.com. Optional. 28 | - _social.linkedin_ - Your LinkedIn public user name, just the name, not the full link. Optional 29 | - _social.twitter_ – Your Twitter user name. Optional. 30 | - _social.github_ – Your GitHub user name. Optional. 31 | - _description_ – The description you want to show up in search engine results. 32 | - _keywords_ – The SEO keywords for this course. An array of strings 33 | - _productionBaseUrl_ – Typically useful for GitHub Pages. This adds a base path to your project. For GitHub Pages, this will be the name of your repo. For example, this site's base URL is `/next-course-starter` because the production URL for this site is `btholt.github.io/next-course-starer`. Do note this will also make your dev server's base URL this as well so you can catch problems before they go to production. 34 | 35 | ### styles/variables.css 36 | 37 | Here is where you can theme your site. You can retheme the whole site with just these. 38 | 39 | ### public/images 40 | 41 | Here is where you should stick all your images. Inside of your markdown, refer to images in this folder as `./images/`. 42 | 43 | Note this site doesn't use `next/image` because that requires the server component. 44 | 45 | ### public/images/author.jpg 46 | 47 | Your image. If you call it this, you won't have to change any code. If you need to change it, it's in `pages/index.js`. 48 | 49 | ### public/images/social-share-cover.jpg 50 | 51 | The image that will be used if someone shares your website to Twitter/Facebook/etc. If you call it this, you won't have to change any code. If you do need to change it, it's in `pages/index.js` 52 | 53 | ### public/images/course-icon.png 54 | 55 | The image at the top of the course. If you call it this, you won't have to change any code. If you do need to change it, it's in `pages/index.js` 56 | 57 | ## Lessons 58 | 59 | All your markdown lesson files will go in `lessons/`. They **must** be organized an named this way: 60 | 61 | The folders must be named `01-section-one-name`, `02-section-two-name`, `03-section-three`, etc. 62 | 63 | The lessons are then grouped inside of these, the lessons are ordered by letters, `A-lesson-one.md`, `B-lesson-two.md`, `C-lesson-three.md`, etc. This numbering scheme matches how Frontend Masters organizes their content. 64 | 65 | The titles of your lessons and sections are generated from the folder and lesson names (and can be overridden.) The first, organizing part of the name is stripped (the `01-` part of `01-section-one` and the `A-` part of `A-lesson-one`), the hyphens are turned into spaces (`section-one` becomes `section one`) and then those are run through [title-case](https://github.com/blakeembrey/change-case#titlecase) (so `section one` becomes `Section One`.) If you need to override these, use the frontmatter (explained below.) 66 | 67 | The folder and lesson names are also used for the slugs. `02-section-two/C-lesson-three.md` becomes `yoursite.com/lessons/section-two/lesson-three`. 68 | 69 | Each of these lessons can have a [frontmatter](https://github.com/jonschlinkert/gray-matter#readme) for the following properties 70 | 71 | - _title_ – If you want the title to be different from the file name, you can specify here what that title should be. Frequently this useful for things where the capitalization would be off e.g. TailwindCSS instead of Tailwindcss. Optional. 72 | - _description_ – If you want to give individual lessons descriptions for SEO and for Frontend Masters, you can write a brief description here. 73 | 74 | Be aware because of how the numbers and letters are stripped out, it is possible to have ambigious paths. `01-welcome/A-intro.md` and `03-welcome/D-intro.md` would resolve to the same thing and only the first one would be visitable. 75 | 76 | ## meta.json 77 | 78 | Each **section** (e.g. inside of `03-section-three` folder) folder can have a meta.json file, and is totally optional. 79 | 80 | - _title_ – an override for the title of the section. Frequently useful for capitalization e.g. `JS Tools` instead of `Js Tools`. 81 | - _icon_ – so you can set the icon used in the home page and the header. These icons are pulled from [the free Font Awesome v5 icons](https://fontawesome.com/v5.15/icons). If you want [fa-hammer](https://fontawesome.com/v5.15/icons/hammer), use "hammer" as the value for icon. 82 | 83 | ## highlight.js Theme 84 | 85 | The code blocks use [Highlight.js](https://highlightjs.org/static/demo/). By default it will use `a11y-light` as the theme for the code blocks. Change the CSS import in `pages/_app.js` to the theme you want to use. 86 | 87 | ## GitHub Pages / GitHub Actions 88 | 89 | By default this repo works with GitHub Pages. Just make sure you set the `productionBaseUrl` in the course.json to the name of the repo. 90 | 91 | It also includes a GitHub Action to automatically deploy to your gh-pages branch. Just make sure that your repo has GitHub Pages enabled and the branch is set to gh-pages. If you're not deploying to GitHub Pages go ahead and delete the `.github` directory. 92 | 93 | By default the GitHub Action looks for a `main` branch, so be sure you're using that instead of `master`. 94 | 95 | ## Example Sites 96 | 97 | - [This repo itself](https://btholt.github.io/next-course-starter/) 98 | - [Complete Intro to React v6](https://btholt.github.io/next-react-v6/) 99 | 100 | ## CSV 101 | 102 | **Not implemented yet, but coming soon**. 103 | 104 | If you run `npm run csv`, a CSV will be generated with all the various lessons' frontmatter outputted to `public/lessons.csv`. You can change the path by changing the `OUTPUT_CSV_PATH` environment variable. 105 | 106 | Another CSV will be output to `public/links.csv` where it pull all the links out of each lesson and put them into a CSV. This path can be modified by setting the `LINKS_CSV_PATH` environment variable. 107 | 108 | ## npm commands 109 | 110 | - `npm run dev` - Next's dev command. Start a local dev server. Note if you have a productionBasePath set in your course.json, your dev server will respect that (so you don't mess up your paths in production.) 111 | - `npm run build` - Build your site for production. This will still include the Next.js server run time. Use this if you're using something like Vercel to host your site. 112 | - `npm run export` - Builds your site statically, use this if you're going to deploy to GitHub Pages, S3, or somewhere else with no server. This will run next build and then next export (no need to run build yourself first.) 113 | - `npm run start` - Start an already-built server. 114 | 115 | ## License 116 | 117 | The **code** is this repo is licensed under the Apache 2.0 license. 118 | 119 | I include the CC-BY-NC-4.0 license for the content; this is what I recommend you license your **content** under: anyone can use and share the content but they cannot sell it; only you can. 120 | -------------------------------------------------------------------------------- /components/corner.js: -------------------------------------------------------------------------------- 1 | export default function Corner() { 2 | return ( 3 |
4 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Gh from "./github"; 3 | import Tw from "./twitter"; 4 | import Li from "./linkedin"; 5 | 6 | export default function Footer({ twitter, linkedin, github }) { 7 | return ( 8 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/github.js: -------------------------------------------------------------------------------- 1 | export default function GitHub() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import Link from "next/link"; 3 | import { Context as HeaderContext } from "../context/headerContext"; 4 | import { Context as CourseContext } from "../context/courseInfoContext"; 5 | 6 | export default function Header(props) { 7 | const [{ section, title, icon }] = useContext(HeaderContext); 8 | const { frontendMastersLink } = useContext(CourseContext); 9 | return ( 10 |
11 |

12 | {props.title} 13 |

14 |
15 | {frontendMastersLink ? ( 16 | 17 | Watch on Frontend Masters 18 | 19 | ) : null} 20 | {section ? ( 21 |

22 | {section} {title} 23 |

24 | ) : null} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/layout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import Footer from "./footer"; 4 | import Header from "./header"; 5 | import getCourseConfig from "../data/course"; 6 | import { Provider as HeaderProvider } from "../context/headerContext"; 7 | import { Provider as CourseInfoProvider } from "../context/courseInfoContext"; 8 | 9 | function Layout({ children }) { 10 | const courseInfo = getCourseConfig(); 11 | const headerHook = useState({}); 12 | return ( 13 | 14 | 15 |
16 |
17 |
18 |
{children}
19 |
20 |
25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default function App({ children }) { 32 | return {children}; 33 | } 34 | -------------------------------------------------------------------------------- /components/linkedin.js: -------------------------------------------------------------------------------- 1 | export default function LinkedIn() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/twitter.js: -------------------------------------------------------------------------------- 1 | export default function Twitter() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /context/courseInfoContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const courseInfoContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = courseInfoContext.Provider; 6 | export const Consumer = courseInfoContext.Consumer; 7 | export const Context = courseInfoContext; 8 | -------------------------------------------------------------------------------- /context/headerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const headerContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = headerContext.Provider; 6 | export const Consumer = headerContext.Consumer; 7 | export const Context = headerContext; 8 | -------------------------------------------------------------------------------- /course.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Brian Holt", 4 | "company": "Stripe" 5 | }, 6 | "title": "Next.js Course Starter Kit", 7 | "subtitle": "for Frontend Masters", 8 | "frontendMastersLink": "https://frontendmasters.com/courses/complete-react-v6/", 9 | "social": { 10 | "linkedin": "btholt", 11 | "github": "btholt", 12 | "twitter": "holtbt" 13 | }, 14 | "description": "A starter kit for making really amazing courses, optimized for Frontend Masters", 15 | "keywords": ["your", "Google", "keywords", "here"], 16 | "productionBaseUrl": "/next-course-starter" 17 | } 18 | -------------------------------------------------------------------------------- /data/course.js: -------------------------------------------------------------------------------- 1 | import config from "../course.json"; 2 | 3 | const DEFAULT_CONFIG = { 4 | author: { 5 | name: "An Author", 6 | company: "An Author's Company", 7 | }, 8 | title: "A Superb Course", 9 | subtitle: "That Teaches Nice Things", 10 | frontendMastersLink: "", 11 | description: "A nice course for nice people.", 12 | keywords: ["a nice course", "for people", "to learn", "nice things"], 13 | social: { 14 | linkedin: "btholt", 15 | github: "btholt", 16 | twitter: "holtbt", 17 | }, 18 | productionBaseUrl: "/", 19 | }; 20 | 21 | export default function getCourseConfig() { 22 | return Object.assign({}, DEFAULT_CONFIG, config); 23 | } 24 | -------------------------------------------------------------------------------- /data/lesson.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import * as matter from "gray-matter"; 4 | import { titleCase } from "title-case"; 5 | import { marked } from "marked"; 6 | import hljs from "highlight.js"; 7 | 8 | marked.setOptions({ 9 | baseUrl: process.env.BASE_URL ? process.env.BASE_URL + "/" : "/", 10 | highlight: function (code, lang) { 11 | const language = hljs.getLanguage(lang) ? lang : "plaintext"; 12 | return hljs.highlight(code, { language }).value; 13 | }, 14 | langPrefix: "hljs language-", 15 | }); 16 | 17 | const DEFAULT_ICON = "info-circle"; 18 | const lessonsPath = path.join(process.env.ROOT, "lessons"); 19 | 20 | function getTitle(slug, override) { 21 | let title = override; 22 | if (!title) { 23 | title = titleCase(slug.split("-").join(" ")); 24 | } 25 | 26 | return title; 27 | } 28 | 29 | async function getMeta(section) { 30 | let meta = {}; 31 | try { 32 | const file = await fs.readFile( 33 | path.join(lessonsPath, section, "meta.json") 34 | ); 35 | meta = JSON.parse(file.toString()); 36 | } catch (e) { 37 | // no meta.json, nothing to do 38 | } 39 | 40 | return meta; 41 | } 42 | 43 | function slugify(inputPath) { 44 | const pathParts = inputPath.split("-"); 45 | const pathOrder = pathParts.shift(); 46 | const pathSlug = pathParts.join("-"); 47 | return { 48 | slug: pathSlug, 49 | order: pathOrder, 50 | title: titleCase(pathParts.join(" ")), 51 | }; 52 | } 53 | 54 | export async function getLessons() { 55 | const dir = await fs.readdir(lessonsPath); 56 | const sections = []; 57 | 58 | for (let dirFilename of dir) { 59 | const dirStats = await fs.lstat(path.join(lessonsPath, dirFilename)); 60 | 61 | if (dirStats.isFile()) { 62 | continue; 63 | } 64 | 65 | const lessonsDir = await fs.readdir(path.join(lessonsPath, dirFilename)); 66 | 67 | let { 68 | title: sectionTitle, 69 | order: sectionOrder, 70 | slug: sectionSlug, 71 | } = slugify(dirFilename); 72 | 73 | let icon = DEFAULT_ICON; 74 | 75 | const meta = await getMeta(dirFilename); 76 | if (meta.title) { 77 | sectionTitle = meta.title; 78 | } 79 | if (meta.icon) { 80 | icon = meta.icon; 81 | } 82 | 83 | const lessons = []; 84 | for (let lessonFilename of lessonsDir) { 85 | if (lessonFilename.slice(-3) !== ".md") { 86 | continue; 87 | } 88 | 89 | const filePath = path.join(lessonsPath, dirFilename, lessonFilename); 90 | 91 | const file = await fs.readFile(filePath); 92 | const { data } = matter(file.toString()); 93 | let slug = lessonFilename.replace(/\.md$/, ""); 94 | 95 | const slugParts = slug.split("-"); 96 | const lessonOrder = slugParts.shift(); 97 | 98 | slug = slugParts.join("-"); 99 | 100 | const title = getTitle(slug, data.title); 101 | 102 | lessons.push({ 103 | slug, 104 | fullSlug: `/lessons/${sectionSlug}/${slug}`, 105 | title, 106 | order: `${sectionOrder}${lessonOrder}`, 107 | path: filePath, 108 | }); 109 | } 110 | 111 | sections.push({ 112 | icon, 113 | title: sectionTitle, 114 | slug: sectionSlug, 115 | lessons, 116 | order: sectionOrder, 117 | }); 118 | } 119 | 120 | return sections; 121 | } 122 | 123 | export async function getLesson(targetDir, targetFile) { 124 | const dir = await fs.readdir(lessonsPath); 125 | 126 | for (let i = 0; i < dir.length; i++) { 127 | const dirPath = dir[i]; 128 | if (dirPath.endsWith(targetDir)) { 129 | const lessonDir = ( 130 | await fs.readdir(path.join(lessonsPath, dirPath)) 131 | ).filter((str) => str.endsWith(".md")); 132 | 133 | for (let j = 0; j < lessonDir.length; j++) { 134 | const slugPath = lessonDir[j]; 135 | if (slugPath.endsWith(targetFile + ".md")) { 136 | const filePath = path.join(lessonsPath, dirPath, slugPath); 137 | const file = await fs.readFile(filePath); 138 | const { data, content } = matter(file.toString()); 139 | const html = marked(content); 140 | const title = getTitle(targetFile, data.title); 141 | const meta = await getMeta(dirPath); 142 | 143 | const section = getTitle(targetDir, meta.title); 144 | const icon = meta.icon ? meta.icon : DEFAULT_ICON; 145 | 146 | let nextSlug; 147 | let prevSlug; 148 | 149 | // get next 150 | if (lessonDir[j + 1]) { 151 | // has next in section 152 | const { slug: next } = slugify(lessonDir[j + 1]); 153 | nextSlug = `${targetDir}/${next.replace(/\.md$/, "")}`; 154 | } else if (dir[i + 1]) { 155 | // has next in next section 156 | const nextDir = ( 157 | await fs.readdir(path.join(lessonsPath, dir[i + 1])) 158 | ).filter((str) => str.endsWith(".md")); 159 | const nextDirSlug = slugify(dir[i + 1]).slug; 160 | const nextLessonSlug = slugify(nextDir[0]).slug.replace( 161 | /\.md$/, 162 | "" 163 | ); 164 | nextSlug = `${nextDirSlug}/${nextLessonSlug}`; 165 | } else { 166 | // last section 167 | nextSlug = null; 168 | } 169 | 170 | // get prev 171 | if (lessonDir[j - 1]) { 172 | // has prev in section 173 | const { slug: prev } = slugify(lessonDir[j - 1]); 174 | prevSlug = `${targetDir}/${prev.replace(/\.md$/, "")}`; 175 | } else if (dir[i - 1]) { 176 | // has prev in prev section 177 | const prevDir = ( 178 | await fs.readdir(path.join(lessonsPath, dir[i - 1])) 179 | ).filter((str) => str.endsWith(".md")); 180 | const prevDirSlug = slugify(dir[i - 1]).slug; 181 | const prevLessonSlug = slugify( 182 | prevDir[prevDir.length - 1] 183 | ).slug.replace(/\.md$/, ""); 184 | prevSlug = `${prevDirSlug}/${prevLessonSlug}`; 185 | } else { 186 | // first section 187 | prevSlug = null; 188 | } 189 | 190 | const base = process.env.BASE_URL ? process.env.BASE_URL : "/"; 191 | 192 | return { 193 | attributes: data, 194 | html, 195 | slug: targetFile, 196 | title, 197 | section, 198 | icon, 199 | filePath, 200 | nextSlug: nextSlug ? path.join(base, "lessons", nextSlug) : null, 201 | prevSlug: prevSlug ? path.join(base, "lessons", prevSlug) : null, 202 | }; 203 | } 204 | } 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | -------------------------------------------------------------------------------- /lessons/01-the-first-section/A-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction" 3 | description: "The introduction to this course." 4 | --- 5 | 6 | Hello! And welcome to the Next.js course starter by [Brian Holt][twitter]. 7 | 8 | ![Cat, dog, and rat logo for the course](./images/BRAND-WHearts.png) 9 | 10 | This is a course starter specifically made for making online courses and optimized for use with [Frontend Masters][fem]. 11 | 12 | [twitter]: https://twitter.com/holtbt 13 | [fem]: https://www.frontendmasters.com 14 | -------------------------------------------------------------------------------- /lessons/02-a-second-section/A-first-lesson.md: -------------------------------------------------------------------------------- 1 | - this 2 | - is 3 | - the 4 | - second 5 | - lesson 6 | -------------------------------------------------------------------------------- /lessons/02-a-second-section/B-second-lesson.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "An important second lesson" 3 | --- 4 | 5 | This is the second lesson 6 | 7 | ```javascript 8 | function getGreeting() { 9 | return `oh hey there`; 10 | } 11 | 12 | console.log(getGreeting()); 13 | ``` 14 | -------------------------------------------------------------------------------- /lessons/02-a-second-section/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "dumpster-fire" 3 | } -------------------------------------------------------------------------------- /lessons/03-thoughts-on-js-and-css/A-css.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CSS" 3 | description: "CSS is for styling" 4 | --- 5 | 6 | CSS is important 7 | -------------------------------------------------------------------------------- /lessons/03-thoughts-on-js-and-css/B-javascript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "JavaScript" 3 | --- 4 | 5 | Seems important, right? 6 | -------------------------------------------------------------------------------- /lessons/03-thoughts-on-js-and-css/C-npm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "npm" 3 | description: "npm doesn't stand for node package manager" 4 | --- 5 | 6 | npm doesn't stand for node package manager 7 | -------------------------------------------------------------------------------- /lessons/03-thoughts-on-js-and-css/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Thoughts on JS and CSS", 3 | "icon": "file-code" 4 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const course = require("./course.json"); 2 | const BASE_URL = course?.productionBaseUrl || ""; 3 | 4 | module.exports = { 5 | basePath: BASE_URL, 6 | env: { 7 | ROOT: __dirname, 8 | BASE_URL, 9 | }, 10 | async redirects() { 11 | if (BASE_URL) { 12 | return [ 13 | { 14 | source: "/", 15 | destination: BASE_URL, 16 | basePath: false, 17 | permanent: false, 18 | }, 19 | ]; 20 | } 21 | return []; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-course-starter", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "license": "(CC-BY-NC-4.0 OR Apache-2.0)", 8 | "dependencies": { 9 | "@fortawesome/fontawesome-free": "^6.1.1", 10 | "gray-matter": "^4.0.3", 11 | "highlight.js": "^11.4.0", 12 | "marked": "^4.0.9", 13 | "next": "^12.0.7", 14 | "react": "^18.0.0", 15 | "react-dom": "^18.0.0", 16 | "title-case": "^3.0.3" 17 | } 18 | }, 19 | "node_modules/@fortawesome/fontawesome-free": { 20 | "version": "6.1.1", 21 | "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", 22 | "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==", 23 | "hasInstallScript": true, 24 | "engines": { 25 | "node": ">=6" 26 | } 27 | }, 28 | "node_modules/@next/env": { 29 | "version": "12.1.4", 30 | "resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.4.tgz", 31 | "integrity": "sha512-7gQwotJDKnfMxxXd8xJ2vsX5AzyDxO3zou0+QOXX8/unypA6icw5+wf6A62yKZ6qQ4UZHHxS68pb6UV+wNneXg==" 32 | }, 33 | "node_modules/@next/swc-android-arm-eabi": { 34 | "version": "12.1.4", 35 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.4.tgz", 36 | "integrity": "sha512-FJg/6a3s2YrUaqZ+/DJZzeZqfxbbWrynQMT1C5wlIEq9aDLXCFpPM/PiOyJh0ahxc0XPmi6uo38Poq+GJTuKWw==", 37 | "cpu": [ 38 | "arm" 39 | ], 40 | "optional": true, 41 | "os": [ 42 | "android" 43 | ], 44 | "engines": { 45 | "node": ">= 10" 46 | } 47 | }, 48 | "node_modules/@next/swc-android-arm64": { 49 | "version": "12.1.4", 50 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.4.tgz", 51 | "integrity": "sha512-LXraazvQQFBgxIg3Htny6G5V5he9EK7oS4jWtMdTGIikmD/OGByOv8ZjLuVLZLtVm3UIvaAiGtlQSLecxJoJDw==", 52 | "cpu": [ 53 | "arm64" 54 | ], 55 | "optional": true, 56 | "os": [ 57 | "android" 58 | ], 59 | "engines": { 60 | "node": ">= 10" 61 | } 62 | }, 63 | "node_modules/@next/swc-darwin-arm64": { 64 | "version": "12.1.4", 65 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.4.tgz", 66 | "integrity": "sha512-SSST/dBymecllZxcqTCcSTCu5o1NKk9I+xcvhn/O9nH6GWjgvGgGkNqLbCarCa0jJ1ukvlBA138FagyrmZ/4rQ==", 67 | "cpu": [ 68 | "arm64" 69 | ], 70 | "optional": true, 71 | "os": [ 72 | "darwin" 73 | ], 74 | "engines": { 75 | "node": ">= 10" 76 | } 77 | }, 78 | "node_modules/@next/swc-darwin-x64": { 79 | "version": "12.1.4", 80 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.4.tgz", 81 | "integrity": "sha512-p1lwdX0TVjaoDXQVuAkjtxVBbCL/urgxiMCBwuPDO7TikpXtSRivi+mIzBj5q7ypgICFmIAOW3TyupXeoPRAnA==", 82 | "cpu": [ 83 | "x64" 84 | ], 85 | "optional": true, 86 | "os": [ 87 | "darwin" 88 | ], 89 | "engines": { 90 | "node": ">= 10" 91 | } 92 | }, 93 | "node_modules/@next/swc-linux-arm-gnueabihf": { 94 | "version": "12.1.4", 95 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.4.tgz", 96 | "integrity": "sha512-67PZlgkCn3TDxacdVft0xqDCL7Io1/C4xbAs0+oSQ0xzp6OzN2RNpuKjHJrJgKd0DsE1XZ9sCP27Qv0591yfyg==", 97 | "cpu": [ 98 | "arm" 99 | ], 100 | "optional": true, 101 | "os": [ 102 | "linux" 103 | ], 104 | "engines": { 105 | "node": ">= 10" 106 | } 107 | }, 108 | "node_modules/@next/swc-linux-arm64-gnu": { 109 | "version": "12.1.4", 110 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.4.tgz", 111 | "integrity": "sha512-OnOWixhhw7aU22TQdQLYrgpgFq0oA1wGgnjAiHJ+St7MLj82KTDyM9UcymAMbGYy6nG/TFOOHdTmRMtCRNOw0g==", 112 | "cpu": [ 113 | "arm64" 114 | ], 115 | "optional": true, 116 | "os": [ 117 | "linux" 118 | ], 119 | "engines": { 120 | "node": ">= 10" 121 | } 122 | }, 123 | "node_modules/@next/swc-linux-arm64-musl": { 124 | "version": "12.1.4", 125 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.4.tgz", 126 | "integrity": "sha512-UoRMzPZnsAavdWtVylYxH8DNC7Uy0i6RrvNwT4PyQVdfANBn2omsUkcH5lgS2O7oaz0nAYLk1vqyZDO7+tJotA==", 127 | "cpu": [ 128 | "arm64" 129 | ], 130 | "optional": true, 131 | "os": [ 132 | "linux" 133 | ], 134 | "engines": { 135 | "node": ">= 10" 136 | } 137 | }, 138 | "node_modules/@next/swc-linux-x64-gnu": { 139 | "version": "12.1.4", 140 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.4.tgz", 141 | "integrity": "sha512-nM+MA/frxlTLUKLJKorctdI20/ugfHRjVEEkcLp/58LGG7slNaP1E5d5dRA1yX6ISjPcQAkywas5VlGCg+uTvA==", 142 | "cpu": [ 143 | "x64" 144 | ], 145 | "optional": true, 146 | "os": [ 147 | "linux" 148 | ], 149 | "engines": { 150 | "node": ">= 10" 151 | } 152 | }, 153 | "node_modules/@next/swc-linux-x64-musl": { 154 | "version": "12.1.4", 155 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.4.tgz", 156 | "integrity": "sha512-GoRHxkuW4u4yKw734B9SzxJwVdyEJosaZ62P7ifOwcujTxhgBt3y76V2nNUrsSuopcKI2ZTDjaa+2wd5zyeXbA==", 157 | "cpu": [ 158 | "x64" 159 | ], 160 | "optional": true, 161 | "os": [ 162 | "linux" 163 | ], 164 | "engines": { 165 | "node": ">= 10" 166 | } 167 | }, 168 | "node_modules/@next/swc-win32-arm64-msvc": { 169 | "version": "12.1.4", 170 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.4.tgz", 171 | "integrity": "sha512-6TQkQze0ievXwHJcVUrIULwCYVe3ccX6T0JgZ1SiMeXpHxISN7VJF/O8uSCw1JvXZYZ6ud0CJ7nfC5HXivgfPg==", 172 | "cpu": [ 173 | "arm64" 174 | ], 175 | "optional": true, 176 | "os": [ 177 | "win32" 178 | ], 179 | "engines": { 180 | "node": ">= 10" 181 | } 182 | }, 183 | "node_modules/@next/swc-win32-ia32-msvc": { 184 | "version": "12.1.4", 185 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.4.tgz", 186 | "integrity": "sha512-CsbX/IXuZ5VSmWCpSetG2HD6VO5FTsO39WNp2IR2Ut/uom9XtLDJAZqjQEnbUTLGHuwDKFjrIO3LkhtROXLE/g==", 187 | "cpu": [ 188 | "ia32" 189 | ], 190 | "optional": true, 191 | "os": [ 192 | "win32" 193 | ], 194 | "engines": { 195 | "node": ">= 10" 196 | } 197 | }, 198 | "node_modules/@next/swc-win32-x64-msvc": { 199 | "version": "12.1.4", 200 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.4.tgz", 201 | "integrity": "sha512-JtYuWzKXKLDMgE/xTcFtCm1MiCIRaAc5XYZfYX3n/ZWSI1SJS/GMm+Su0SAHJgRFavJh6U/p998YwO/iGTIgqQ==", 202 | "cpu": [ 203 | "x64" 204 | ], 205 | "optional": true, 206 | "os": [ 207 | "win32" 208 | ], 209 | "engines": { 210 | "node": ">= 10" 211 | } 212 | }, 213 | "node_modules/argparse": { 214 | "version": "1.0.10", 215 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 216 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 217 | "dependencies": { 218 | "sprintf-js": "~1.0.2" 219 | } 220 | }, 221 | "node_modules/caniuse-lite": { 222 | "version": "1.0.30001324", 223 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001324.tgz", 224 | "integrity": "sha512-/eYp1J6zYh1alySQB4uzYFkLmxxI8tk0kxldbNHXp8+v+rdMKdUBNjRLz7T7fz6Iox+1lIdYpc7rq6ZcXfTukg==", 225 | "funding": [ 226 | { 227 | "type": "opencollective", 228 | "url": "https://opencollective.com/browserslist" 229 | }, 230 | { 231 | "type": "tidelift", 232 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 233 | } 234 | ] 235 | }, 236 | "node_modules/esprima": { 237 | "version": "4.0.1", 238 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 239 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 240 | "bin": { 241 | "esparse": "bin/esparse.js", 242 | "esvalidate": "bin/esvalidate.js" 243 | }, 244 | "engines": { 245 | "node": ">=4" 246 | } 247 | }, 248 | "node_modules/extend-shallow": { 249 | "version": "2.0.1", 250 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 251 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 252 | "dependencies": { 253 | "is-extendable": "^0.1.0" 254 | }, 255 | "engines": { 256 | "node": ">=0.10.0" 257 | } 258 | }, 259 | "node_modules/gray-matter": { 260 | "version": "4.0.3", 261 | "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", 262 | "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", 263 | "dependencies": { 264 | "js-yaml": "^3.13.1", 265 | "kind-of": "^6.0.2", 266 | "section-matter": "^1.0.0", 267 | "strip-bom-string": "^1.0.0" 268 | }, 269 | "engines": { 270 | "node": ">=6.0" 271 | } 272 | }, 273 | "node_modules/highlight.js": { 274 | "version": "11.5.0", 275 | "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.0.tgz", 276 | "integrity": "sha512-SM6WDj5/C+VfIY8pZ6yW6Xa0Fm1tniYVYWYW1Q/DcMnISZFrC3aQAZZZFAAZtybKNrGId3p/DNbFTtcTXXgYBw==", 277 | "engines": { 278 | "node": ">=12.0.0" 279 | } 280 | }, 281 | "node_modules/is-extendable": { 282 | "version": "0.1.1", 283 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 284 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", 285 | "engines": { 286 | "node": ">=0.10.0" 287 | } 288 | }, 289 | "node_modules/js-tokens": { 290 | "version": "4.0.0", 291 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 292 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 293 | }, 294 | "node_modules/js-yaml": { 295 | "version": "3.14.1", 296 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 297 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 298 | "dependencies": { 299 | "argparse": "^1.0.7", 300 | "esprima": "^4.0.0" 301 | }, 302 | "bin": { 303 | "js-yaml": "bin/js-yaml.js" 304 | } 305 | }, 306 | "node_modules/kind-of": { 307 | "version": "6.0.3", 308 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 309 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 310 | "engines": { 311 | "node": ">=0.10.0" 312 | } 313 | }, 314 | "node_modules/loose-envify": { 315 | "version": "1.4.0", 316 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 317 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 318 | "dependencies": { 319 | "js-tokens": "^3.0.0 || ^4.0.0" 320 | }, 321 | "bin": { 322 | "loose-envify": "cli.js" 323 | } 324 | }, 325 | "node_modules/marked": { 326 | "version": "4.0.12", 327 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 328 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", 329 | "bin": { 330 | "marked": "bin/marked.js" 331 | }, 332 | "engines": { 333 | "node": ">= 12" 334 | } 335 | }, 336 | "node_modules/nanoid": { 337 | "version": "3.3.2", 338 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", 339 | "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", 340 | "bin": { 341 | "nanoid": "bin/nanoid.cjs" 342 | }, 343 | "engines": { 344 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 345 | } 346 | }, 347 | "node_modules/next": { 348 | "version": "12.1.4", 349 | "resolved": "https://registry.npmjs.org/next/-/next-12.1.4.tgz", 350 | "integrity": "sha512-DA4g97BM4Z0nKtDvCTm58RxdvoQyYzeg0AeVbh0N4Y/D8ELrNu47lQeEgRGF8hV4eQ+Sal90zxrJQQG/mPQ8CQ==", 351 | "dependencies": { 352 | "@next/env": "12.1.4", 353 | "caniuse-lite": "^1.0.30001283", 354 | "postcss": "8.4.5", 355 | "styled-jsx": "5.0.1" 356 | }, 357 | "bin": { 358 | "next": "dist/bin/next" 359 | }, 360 | "engines": { 361 | "node": ">=12.22.0" 362 | }, 363 | "optionalDependencies": { 364 | "@next/swc-android-arm-eabi": "12.1.4", 365 | "@next/swc-android-arm64": "12.1.4", 366 | "@next/swc-darwin-arm64": "12.1.4", 367 | "@next/swc-darwin-x64": "12.1.4", 368 | "@next/swc-linux-arm-gnueabihf": "12.1.4", 369 | "@next/swc-linux-arm64-gnu": "12.1.4", 370 | "@next/swc-linux-arm64-musl": "12.1.4", 371 | "@next/swc-linux-x64-gnu": "12.1.4", 372 | "@next/swc-linux-x64-musl": "12.1.4", 373 | "@next/swc-win32-arm64-msvc": "12.1.4", 374 | "@next/swc-win32-ia32-msvc": "12.1.4", 375 | "@next/swc-win32-x64-msvc": "12.1.4" 376 | }, 377 | "peerDependencies": { 378 | "fibers": ">= 3.1.0", 379 | "node-sass": "^6.0.0 || ^7.0.0", 380 | "react": "^17.0.2 || ^18.0.0-0", 381 | "react-dom": "^17.0.2 || ^18.0.0-0", 382 | "sass": "^1.3.0" 383 | }, 384 | "peerDependenciesMeta": { 385 | "fibers": { 386 | "optional": true 387 | }, 388 | "node-sass": { 389 | "optional": true 390 | }, 391 | "sass": { 392 | "optional": true 393 | } 394 | } 395 | }, 396 | "node_modules/picocolors": { 397 | "version": "1.0.0", 398 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 399 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 400 | }, 401 | "node_modules/postcss": { 402 | "version": "8.4.5", 403 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", 404 | "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", 405 | "dependencies": { 406 | "nanoid": "^3.1.30", 407 | "picocolors": "^1.0.0", 408 | "source-map-js": "^1.0.1" 409 | }, 410 | "engines": { 411 | "node": "^10 || ^12 || >=14" 412 | }, 413 | "funding": { 414 | "type": "opencollective", 415 | "url": "https://opencollective.com/postcss/" 416 | } 417 | }, 418 | "node_modules/react": { 419 | "version": "18.0.0", 420 | "resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz", 421 | "integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==", 422 | "dependencies": { 423 | "loose-envify": "^1.1.0" 424 | }, 425 | "engines": { 426 | "node": ">=0.10.0" 427 | } 428 | }, 429 | "node_modules/react-dom": { 430 | "version": "18.0.0", 431 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz", 432 | "integrity": "sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==", 433 | "dependencies": { 434 | "loose-envify": "^1.1.0", 435 | "scheduler": "^0.21.0" 436 | }, 437 | "peerDependencies": { 438 | "react": "^18.0.0" 439 | } 440 | }, 441 | "node_modules/scheduler": { 442 | "version": "0.21.0", 443 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", 444 | "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", 445 | "dependencies": { 446 | "loose-envify": "^1.1.0" 447 | } 448 | }, 449 | "node_modules/section-matter": { 450 | "version": "1.0.0", 451 | "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", 452 | "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", 453 | "dependencies": { 454 | "extend-shallow": "^2.0.1", 455 | "kind-of": "^6.0.0" 456 | }, 457 | "engines": { 458 | "node": ">=4" 459 | } 460 | }, 461 | "node_modules/source-map-js": { 462 | "version": "1.0.2", 463 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 464 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 465 | "engines": { 466 | "node": ">=0.10.0" 467 | } 468 | }, 469 | "node_modules/sprintf-js": { 470 | "version": "1.0.3", 471 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 472 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 473 | }, 474 | "node_modules/strip-bom-string": { 475 | "version": "1.0.0", 476 | "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", 477 | "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", 478 | "engines": { 479 | "node": ">=0.10.0" 480 | } 481 | }, 482 | "node_modules/styled-jsx": { 483 | "version": "5.0.1", 484 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz", 485 | "integrity": "sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==", 486 | "engines": { 487 | "node": ">= 12.0.0" 488 | }, 489 | "peerDependencies": { 490 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 491 | }, 492 | "peerDependenciesMeta": { 493 | "@babel/core": { 494 | "optional": true 495 | }, 496 | "babel-plugin-macros": { 497 | "optional": true 498 | } 499 | } 500 | }, 501 | "node_modules/title-case": { 502 | "version": "3.0.3", 503 | "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", 504 | "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", 505 | "dependencies": { 506 | "tslib": "^2.0.3" 507 | } 508 | }, 509 | "node_modules/tslib": { 510 | "version": "2.3.1", 511 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 512 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 513 | } 514 | }, 515 | "dependencies": { 516 | "@fortawesome/fontawesome-free": { 517 | "version": "6.1.1", 518 | "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.1.1.tgz", 519 | "integrity": "sha512-J/3yg2AIXc9wznaVqpHVX3Wa5jwKovVF0AMYSnbmcXTiL3PpRPfF58pzWucCwEiCJBp+hCNRLWClTomD8SseKg==" 520 | }, 521 | "@next/env": { 522 | "version": "12.1.4", 523 | "resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.4.tgz", 524 | "integrity": "sha512-7gQwotJDKnfMxxXd8xJ2vsX5AzyDxO3zou0+QOXX8/unypA6icw5+wf6A62yKZ6qQ4UZHHxS68pb6UV+wNneXg==" 525 | }, 526 | "@next/swc-android-arm-eabi": { 527 | "version": "12.1.4", 528 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.4.tgz", 529 | "integrity": "sha512-FJg/6a3s2YrUaqZ+/DJZzeZqfxbbWrynQMT1C5wlIEq9aDLXCFpPM/PiOyJh0ahxc0XPmi6uo38Poq+GJTuKWw==", 530 | "optional": true 531 | }, 532 | "@next/swc-android-arm64": { 533 | "version": "12.1.4", 534 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.4.tgz", 535 | "integrity": "sha512-LXraazvQQFBgxIg3Htny6G5V5he9EK7oS4jWtMdTGIikmD/OGByOv8ZjLuVLZLtVm3UIvaAiGtlQSLecxJoJDw==", 536 | "optional": true 537 | }, 538 | "@next/swc-darwin-arm64": { 539 | "version": "12.1.4", 540 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.4.tgz", 541 | "integrity": "sha512-SSST/dBymecllZxcqTCcSTCu5o1NKk9I+xcvhn/O9nH6GWjgvGgGkNqLbCarCa0jJ1ukvlBA138FagyrmZ/4rQ==", 542 | "optional": true 543 | }, 544 | "@next/swc-darwin-x64": { 545 | "version": "12.1.4", 546 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.4.tgz", 547 | "integrity": "sha512-p1lwdX0TVjaoDXQVuAkjtxVBbCL/urgxiMCBwuPDO7TikpXtSRivi+mIzBj5q7ypgICFmIAOW3TyupXeoPRAnA==", 548 | "optional": true 549 | }, 550 | "@next/swc-linux-arm-gnueabihf": { 551 | "version": "12.1.4", 552 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.4.tgz", 553 | "integrity": "sha512-67PZlgkCn3TDxacdVft0xqDCL7Io1/C4xbAs0+oSQ0xzp6OzN2RNpuKjHJrJgKd0DsE1XZ9sCP27Qv0591yfyg==", 554 | "optional": true 555 | }, 556 | "@next/swc-linux-arm64-gnu": { 557 | "version": "12.1.4", 558 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.4.tgz", 559 | "integrity": "sha512-OnOWixhhw7aU22TQdQLYrgpgFq0oA1wGgnjAiHJ+St7MLj82KTDyM9UcymAMbGYy6nG/TFOOHdTmRMtCRNOw0g==", 560 | "optional": true 561 | }, 562 | "@next/swc-linux-arm64-musl": { 563 | "version": "12.1.4", 564 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.4.tgz", 565 | "integrity": "sha512-UoRMzPZnsAavdWtVylYxH8DNC7Uy0i6RrvNwT4PyQVdfANBn2omsUkcH5lgS2O7oaz0nAYLk1vqyZDO7+tJotA==", 566 | "optional": true 567 | }, 568 | "@next/swc-linux-x64-gnu": { 569 | "version": "12.1.4", 570 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.4.tgz", 571 | "integrity": "sha512-nM+MA/frxlTLUKLJKorctdI20/ugfHRjVEEkcLp/58LGG7slNaP1E5d5dRA1yX6ISjPcQAkywas5VlGCg+uTvA==", 572 | "optional": true 573 | }, 574 | "@next/swc-linux-x64-musl": { 575 | "version": "12.1.4", 576 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.4.tgz", 577 | "integrity": "sha512-GoRHxkuW4u4yKw734B9SzxJwVdyEJosaZ62P7ifOwcujTxhgBt3y76V2nNUrsSuopcKI2ZTDjaa+2wd5zyeXbA==", 578 | "optional": true 579 | }, 580 | "@next/swc-win32-arm64-msvc": { 581 | "version": "12.1.4", 582 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.4.tgz", 583 | "integrity": "sha512-6TQkQze0ievXwHJcVUrIULwCYVe3ccX6T0JgZ1SiMeXpHxISN7VJF/O8uSCw1JvXZYZ6ud0CJ7nfC5HXivgfPg==", 584 | "optional": true 585 | }, 586 | "@next/swc-win32-ia32-msvc": { 587 | "version": "12.1.4", 588 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.4.tgz", 589 | "integrity": "sha512-CsbX/IXuZ5VSmWCpSetG2HD6VO5FTsO39WNp2IR2Ut/uom9XtLDJAZqjQEnbUTLGHuwDKFjrIO3LkhtROXLE/g==", 590 | "optional": true 591 | }, 592 | "@next/swc-win32-x64-msvc": { 593 | "version": "12.1.4", 594 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.4.tgz", 595 | "integrity": "sha512-JtYuWzKXKLDMgE/xTcFtCm1MiCIRaAc5XYZfYX3n/ZWSI1SJS/GMm+Su0SAHJgRFavJh6U/p998YwO/iGTIgqQ==", 596 | "optional": true 597 | }, 598 | "argparse": { 599 | "version": "1.0.10", 600 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 601 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 602 | "requires": { 603 | "sprintf-js": "~1.0.2" 604 | } 605 | }, 606 | "caniuse-lite": { 607 | "version": "1.0.30001324", 608 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001324.tgz", 609 | "integrity": "sha512-/eYp1J6zYh1alySQB4uzYFkLmxxI8tk0kxldbNHXp8+v+rdMKdUBNjRLz7T7fz6Iox+1lIdYpc7rq6ZcXfTukg==" 610 | }, 611 | "esprima": { 612 | "version": "4.0.1", 613 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 614 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 615 | }, 616 | "extend-shallow": { 617 | "version": "2.0.1", 618 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 619 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 620 | "requires": { 621 | "is-extendable": "^0.1.0" 622 | } 623 | }, 624 | "gray-matter": { 625 | "version": "4.0.3", 626 | "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", 627 | "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", 628 | "requires": { 629 | "js-yaml": "^3.13.1", 630 | "kind-of": "^6.0.2", 631 | "section-matter": "^1.0.0", 632 | "strip-bom-string": "^1.0.0" 633 | } 634 | }, 635 | "highlight.js": { 636 | "version": "11.5.0", 637 | "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.0.tgz", 638 | "integrity": "sha512-SM6WDj5/C+VfIY8pZ6yW6Xa0Fm1tniYVYWYW1Q/DcMnISZFrC3aQAZZZFAAZtybKNrGId3p/DNbFTtcTXXgYBw==" 639 | }, 640 | "is-extendable": { 641 | "version": "0.1.1", 642 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 643 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" 644 | }, 645 | "js-tokens": { 646 | "version": "4.0.0", 647 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 648 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 649 | }, 650 | "js-yaml": { 651 | "version": "3.14.1", 652 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 653 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 654 | "requires": { 655 | "argparse": "^1.0.7", 656 | "esprima": "^4.0.0" 657 | } 658 | }, 659 | "kind-of": { 660 | "version": "6.0.3", 661 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 662 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" 663 | }, 664 | "loose-envify": { 665 | "version": "1.4.0", 666 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 667 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 668 | "requires": { 669 | "js-tokens": "^3.0.0 || ^4.0.0" 670 | } 671 | }, 672 | "marked": { 673 | "version": "4.0.12", 674 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", 675 | "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==" 676 | }, 677 | "nanoid": { 678 | "version": "3.3.2", 679 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", 680 | "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==" 681 | }, 682 | "next": { 683 | "version": "12.1.4", 684 | "resolved": "https://registry.npmjs.org/next/-/next-12.1.4.tgz", 685 | "integrity": "sha512-DA4g97BM4Z0nKtDvCTm58RxdvoQyYzeg0AeVbh0N4Y/D8ELrNu47lQeEgRGF8hV4eQ+Sal90zxrJQQG/mPQ8CQ==", 686 | "requires": { 687 | "@next/env": "12.1.4", 688 | "@next/swc-android-arm-eabi": "12.1.4", 689 | "@next/swc-android-arm64": "12.1.4", 690 | "@next/swc-darwin-arm64": "12.1.4", 691 | "@next/swc-darwin-x64": "12.1.4", 692 | "@next/swc-linux-arm-gnueabihf": "12.1.4", 693 | "@next/swc-linux-arm64-gnu": "12.1.4", 694 | "@next/swc-linux-arm64-musl": "12.1.4", 695 | "@next/swc-linux-x64-gnu": "12.1.4", 696 | "@next/swc-linux-x64-musl": "12.1.4", 697 | "@next/swc-win32-arm64-msvc": "12.1.4", 698 | "@next/swc-win32-ia32-msvc": "12.1.4", 699 | "@next/swc-win32-x64-msvc": "12.1.4", 700 | "caniuse-lite": "^1.0.30001283", 701 | "postcss": "8.4.5", 702 | "styled-jsx": "5.0.1" 703 | } 704 | }, 705 | "picocolors": { 706 | "version": "1.0.0", 707 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 708 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 709 | }, 710 | "postcss": { 711 | "version": "8.4.5", 712 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", 713 | "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", 714 | "requires": { 715 | "nanoid": "^3.1.30", 716 | "picocolors": "^1.0.0", 717 | "source-map-js": "^1.0.1" 718 | } 719 | }, 720 | "react": { 721 | "version": "18.0.0", 722 | "resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz", 723 | "integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==", 724 | "requires": { 725 | "loose-envify": "^1.1.0" 726 | } 727 | }, 728 | "react-dom": { 729 | "version": "18.0.0", 730 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz", 731 | "integrity": "sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==", 732 | "requires": { 733 | "loose-envify": "^1.1.0", 734 | "scheduler": "^0.21.0" 735 | } 736 | }, 737 | "scheduler": { 738 | "version": "0.21.0", 739 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", 740 | "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", 741 | "requires": { 742 | "loose-envify": "^1.1.0" 743 | } 744 | }, 745 | "section-matter": { 746 | "version": "1.0.0", 747 | "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", 748 | "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", 749 | "requires": { 750 | "extend-shallow": "^2.0.1", 751 | "kind-of": "^6.0.0" 752 | } 753 | }, 754 | "source-map-js": { 755 | "version": "1.0.2", 756 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 757 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 758 | }, 759 | "sprintf-js": { 760 | "version": "1.0.3", 761 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 762 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 763 | }, 764 | "strip-bom-string": { 765 | "version": "1.0.0", 766 | "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", 767 | "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" 768 | }, 769 | "styled-jsx": { 770 | "version": "5.0.1", 771 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz", 772 | "integrity": "sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==", 773 | "requires": {} 774 | }, 775 | "title-case": { 776 | "version": "3.0.3", 777 | "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", 778 | "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", 779 | "requires": { 780 | "tslib": "^2.0.3" 781 | } 782 | }, 783 | "tslib": { 784 | "version": "2.3.1", 785 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 786 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 787 | } 788 | } 789 | } 790 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "(CC-BY-NC-4.0 OR Apache-2.0)", 4 | "author": "Brian Holt ", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next build && next export", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-free": "^6.1.1", 13 | "gray-matter": "^4.0.3", 14 | "highlight.js": "^11.4.0", 15 | "marked": "^4.0.9", 16 | "next": "^12.0.7", 17 | "react": "^18.0.0", 18 | "react-dom": "^18.0.0", 19 | "title-case": "^3.0.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@fortawesome/fontawesome-free/js/all.js"; 2 | 3 | import "highlight.js/styles/a11y-light.css"; 4 | import "../styles/variables.css"; 5 | import "../styles/footer.css"; 6 | import "../styles/courses.css"; 7 | 8 | import Layout from "../components/layout"; 9 | 10 | // TODO favicons 11 | 12 | export default function App({ Component, pageProps }) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | 4 | import { getLessons } from "../data/lesson"; 5 | 6 | import Corner from "../components/corner"; 7 | import getCourseConfig from "../data/course"; 8 | 9 | export default function Lessons({ sections }) { 10 | const courseInfo = getCourseConfig(); 11 | return ( 12 | <> 13 | 14 | {courseInfo.title} 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |

{courseInfo.title}

30 |

{courseInfo.subtitle}

31 |
32 |
33 | author image 38 |
39 |
40 |
{courseInfo.author.name}
41 |
{courseInfo.author.company}
42 |
43 |
44 |
45 |
46 |
47 | course icon 51 |
52 |
53 | {courseInfo.frontendMastersLink ? ( 54 | 55 | Watch on Frontend Masters 56 | 57 | ) : null} 58 |
59 |

Table of Contents

60 |
61 |
    62 | {sections.map((section) => ( 63 |
  1. 64 |
    65 |
    66 | 67 |
    68 |
    69 |

    {section.title}

    70 |
      71 | {section.lessons.map((lesson) => ( 72 |
    1. 73 | {lesson.title} 74 |
    2. 75 | ))} 76 |
    77 |
    78 | 79 |
    80 |
  2. 81 | ))} 82 |
83 |
84 |
85 |
86 | 87 | ); 88 | } 89 | 90 | export async function getStaticProps() { 91 | const sections = await getLessons(); 92 | return { 93 | props: { 94 | sections, 95 | }, 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /pages/lessons/[section]/[slug].js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { getLesson, getLessons } from "../../../data/lesson"; 4 | import getCourseConfig from "../../../data/course"; 5 | import Corner from "../../../components/corner"; 6 | import { Context } from "../../../context/headerContext"; 7 | 8 | // export const meta = (routeData) => { 9 | // return { 10 | // title: `${routeData.data.section} – ${routeData.data.title}`, 11 | // description: routeData.data.attributes.description, 12 | // }; 13 | // }; 14 | 15 | export default function LessonSlug({ post }) { 16 | const courseInfo = getCourseConfig(); 17 | const [_, setHeader] = useContext(Context); 18 | useEffect(() => { 19 | setHeader({ 20 | section: post.section, 21 | title: post.title, 22 | icon: post.icon, 23 | }); 24 | return () => setHeader({}); 25 | }, []); 26 | 27 | const title = post.title 28 | ? `${post.title} – ${courseInfo.title}` 29 | : courseInfo.title; 30 | const description = post.description 31 | ? post.description 32 | : courseInfo.description; 33 | 34 | return ( 35 | <> 36 | 37 | {title} 38 | 39 | {/* */} 40 | 41 | 42 | 46 | 47 | 48 |
49 |
50 |
54 |
55 | {post.prevSlug ? ( 56 | 57 | ← Previous 58 | 59 | ) : null} 60 | {post.nextSlug ? ( 61 | 62 | Next → 63 | 64 | ) : null} 65 |
66 |
67 | 68 |
69 | 70 | ); 71 | } 72 | 73 | export async function getStaticProps({ params }) { 74 | const post = await getLesson(params.section, params.slug); 75 | return { 76 | props: { 77 | post, 78 | }, 79 | }; 80 | } 81 | 82 | export async function getStaticPaths() { 83 | const sections = await getLessons(); 84 | const lessons = sections.map((section) => section.lessons); 85 | const slugs = lessons.flat().map((lesson) => lesson.fullSlug); 86 | 87 | return { paths: slugs, fallback: false }; 88 | } 89 | -------------------------------------------------------------------------------- /public/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/next-course-starter/d84a313a55fc52e4d1190dbb074dc2b410b1d4fa/public/.nojekyll -------------------------------------------------------------------------------- /public/images/BRAND-WHearts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/next-course-starter/d84a313a55fc52e4d1190dbb074dc2b410b1d4fa/public/images/BRAND-WHearts.png -------------------------------------------------------------------------------- /public/images/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/next-course-starter/d84a313a55fc52e4d1190dbb074dc2b410b1d4fa/public/images/author.jpg -------------------------------------------------------------------------------- /public/images/course-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/next-course-starter/d84a313a55fc52e4d1190dbb074dc2b410b1d4fa/public/images/course-icon.png -------------------------------------------------------------------------------- /public/images/social-share-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-dev-dragon/next-course-starter/d84a313a55fc52e4d1190dbb074dc2b410b1d4fa/public/images/social-share-cover.jpg -------------------------------------------------------------------------------- /styles/courses.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | 3 | /* mini css reset */ 4 | html { 5 | font-size: 16px; 6 | } 7 | 8 | body, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | p, 16 | ol, 17 | ul { 18 | margin: 0; 19 | padding: 0; 20 | font-weight: normal; 21 | } 22 | 23 | img { 24 | max-width: 100%; 25 | height: auto; 26 | } 27 | 28 | * { 29 | box-sizing: border-box; 30 | } 31 | 32 | body { 33 | font-family: "Open Sans"; 34 | background: linear-gradient(90deg, var(--bg-main) 15px, transparent 1%) center, 35 | linear-gradient(var(--bg-main) 15px, transparent 1%) center, var(--bg-dots); 36 | background-size: 16px 16px; 37 | margin: 0; 38 | } 39 | 40 | a { 41 | color: var(--text-links); 42 | text-decoration: none; 43 | } 44 | 45 | .navbar { 46 | border-bottom: 1px solid #ccc; 47 | position: fixed; 48 | width: 100%; 49 | top: 0; 50 | z-index: 10; 51 | display: flex; 52 | justify-content: space-between; 53 | align-items: center; 54 | background-color: var(--bg-main); 55 | padding: 10px; 56 | } 57 | 58 | .navbar h1 { 59 | font-size: 20px; 60 | margin: inherit; 61 | padding: inherit; 62 | font-weight: bold; 63 | color: var(--text-main); 64 | } 65 | 66 | .navbar h2 { 67 | font-size: 14px; 68 | margin: inherit; 69 | margin-left: 15px; 70 | padding: inherit; 71 | text-transform: uppercase; 72 | } 73 | 74 | .navbar-info { 75 | display: flex; 76 | flex-direction: row; 77 | align-items: center; 78 | justify-content: center; 79 | } 80 | 81 | header .cta-btn { 82 | display: none; /* only displays at large screen sizes */ 83 | } 84 | 85 | .main .cta-btn { 86 | width: 90%; 87 | margin: 20px auto 0px auto; 88 | max-width: 500px; 89 | padding: 12px 20px; 90 | } 91 | 92 | .cta-btn { 93 | border-radius: 10px; 94 | background: var(--nav-buttons); 95 | color: var(--nav-buttons-text); 96 | padding: 7px 20px; 97 | display: flex; 98 | justify-content: center; 99 | align-items: center; 100 | } 101 | 102 | .jumbotron { 103 | padding: 0; 104 | } 105 | 106 | .jumbotron .courseInfo, 107 | .jumbotron .courseIcon { 108 | padding: 20px; 109 | } 110 | 111 | .jumbotron .courseInfo, 112 | .jumbotron .courseIcon { 113 | text-align: center; 114 | } 115 | 116 | .author { 117 | margin-top: 40px; 118 | display: flex; 119 | justify-content: center; 120 | } 121 | 122 | @media (min-width: 1000px) { 123 | header .cta-btn { 124 | display: flex; 125 | } 126 | 127 | .main .cta-btn { 128 | display: none; 129 | } 130 | 131 | .jumbotron { 132 | display: flex; 133 | width: 100%; 134 | min-height: 45vh; 135 | } 136 | .jumbotron .courseInfo, 137 | .jumbotron .courseIcon { 138 | display: flex; 139 | justify-content: center; 140 | align-items: center; 141 | } 142 | .jumbotron .courseInfo { 143 | width: 65%; 144 | text-align: right; 145 | } 146 | .jumbotron .courseIcon { 147 | width: 35%; 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | } 152 | 153 | .author { 154 | justify-content: flex-end; 155 | } 156 | .jumbotron .courseInfo-inner { 157 | max-width: 85%; 158 | } 159 | } 160 | 161 | .jumbotron h1, 162 | .jumbotron h2 { 163 | color: var(--text-main-headers); 164 | } 165 | 166 | .jumbotron h1 { 167 | font-size: 50px; 168 | margin-bottom: 20px; 169 | } 170 | 171 | .jumbotron .courseInfo { 172 | background: var(--primary); 173 | } 174 | 175 | .jumbotron .courseIcon { 176 | background: var(--secondary); 177 | } 178 | 179 | .jumbotron .courseIcon img { 180 | max-width: 180px; 181 | } 182 | 183 | .author .info { 184 | padding: 10px; 185 | } 186 | 187 | .author .name { 188 | font-size: 18px; 189 | font-weight: bold; 190 | color: var(--text-main-headers); 191 | } 192 | 193 | .author .company { 194 | color: var(--text-main-headers); 195 | font-size: 16px; 196 | } 197 | 198 | .author .image { 199 | border-radius: 75px; 200 | overflow: hidden; 201 | height: 75px; 202 | width: 75px; 203 | } 204 | 205 | .navbar-brand.navbar-brand a { 206 | text-transform: uppercase; 207 | font-weight: bold; 208 | color: var(--text-main-headers); 209 | } 210 | 211 | .lesson-section-title { 212 | color: var(--text-main-headers); 213 | } 214 | 215 | .lesson-container { 216 | position: relative; 217 | max-width: 900px; 218 | margin: 0 auto 45px auto; 219 | padding: 10px 40px; 220 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 221 | background-color: var(--bg-lesson); 222 | border-radius: 5px; 223 | margin-top: 40px; 224 | } 225 | 226 | .lesson { 227 | margin: 15px; 228 | padding: 15px; 229 | border-radius: 8px; 230 | } 231 | 232 | .lesson > h1 { 233 | color: var(--text-header); 234 | font-size: 24px; 235 | } 236 | 237 | .lesson h2 { 238 | font-size: 24px; 239 | margin-top: 20px; 240 | margin-bottom: 10px; 241 | } 242 | 243 | .lesson h2::after { 244 | content: ""; 245 | display: block; 246 | height: 3px; 247 | margin-top: 5px; 248 | background: var(--text-header); 249 | max-width: 300px; 250 | } 251 | 252 | .lesson p { 253 | clear: both; 254 | } 255 | 256 | .lesson p, 257 | .lesson li { 258 | line-height: 180%; 259 | } 260 | 261 | .lesson-links { 262 | font-size: 18px; 263 | padding: 15px 0; 264 | } 265 | 266 | .next { 267 | float: right; 268 | } 269 | 270 | .prev { 271 | float: left; 272 | } 273 | 274 | .lesson-title { 275 | text-transform: uppercase; 276 | font-weight: bold; 277 | } 278 | 279 | .gatsby-highlight { 280 | padding: 4px; 281 | border-radius: 4px; 282 | display: flex; 283 | justify-content: space-between; 284 | flex-direction: column; 285 | align-items: stretch; 286 | } 287 | 288 | .lesson-content td { 289 | border: 1px solid black; 290 | padding: 8px; 291 | } 292 | 293 | .lesson-content td input { 294 | min-width: 300px; 295 | } 296 | 297 | .lesson-content img { 298 | margin: 5px auto; 299 | display: block; 300 | } 301 | 302 | .lesson-flex { 303 | display: flex; 304 | flex-direction: column; 305 | justify-content: center; 306 | align-items: center; 307 | } 308 | 309 | .random-tweet { 310 | width: 100%; 311 | margin-top: 100px; 312 | } 313 | 314 | .fem-link { 315 | text-align: center; 316 | } 317 | 318 | .content-container { 319 | display: flex; 320 | flex-direction: column; 321 | justify-content: space-between; 322 | min-height: 100vh; 323 | padding-top: 50px; 324 | } 325 | 326 | blockquote { 327 | padding: 15px; 328 | background-color: var(--emphasized-bg); 329 | border: 2px solid var(--emphasized-border); 330 | border-radius: 5px; 331 | width: 100%; 332 | margin: 10px 0; 333 | } 334 | 335 | blockquote > *:last-child { 336 | margin-bottom: 0; 337 | } 338 | 339 | .lesson-content img { 340 | max-width: 100%; 341 | } 342 | 343 | .main-card { 344 | max-width: 900px; 345 | margin: 0 auto; 346 | overflow: hidden; 347 | } 348 | 349 | .lesson-title { 350 | font-size: 20px; 351 | padding: 15px 30px; 352 | } 353 | 354 | .lesson-content { 355 | line-height: 1.5; 356 | } 357 | 358 | .lesson-text { 359 | width: 100%; 360 | padding: 25px 5px 25px 35px; 361 | min-height: 200px; 362 | } 363 | 364 | .sections-name { 365 | margin: 0; 366 | padding: 0; 367 | } 368 | 369 | ol.sections-name { 370 | counter-reset: my-awesome-counter; 371 | list-style: none; 372 | padding-left: 40px; 373 | width: 98%; 374 | margin: 0; 375 | padding: 0; 376 | } 377 | 378 | ol.sections-name > li { 379 | counter-increment: my-awesome-counter; 380 | display: flex; 381 | flex-direction: row; 382 | flex-wrap: wrap; 383 | margin-bottom: 35px; 384 | width: 100%; 385 | box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2); 386 | border-bottom-right-radius: 5px; 387 | border-top-right-radius: 5px; 388 | } 389 | ol.sections-name .lesson-preface { 390 | /* content: counter(my-awesome-counter); */ 391 | color: var(--icons); 392 | display: flex; 393 | position: relative; 394 | align-items: center; 395 | justify-content: center; 396 | background: #93aca7; 397 | font-size: 30px; 398 | padding: 25px; 399 | width: 30%; 400 | } 401 | 402 | .lesson-preface.lesson-preface > svg { 403 | width: 80%; 404 | height: inherit; 405 | max-height: 100px; 406 | } 407 | 408 | ol.sections-name .lesson-preface::before { 409 | content: counter(my-awesome-counter); 410 | position: absolute; 411 | top: 0; 412 | left: 5px; 413 | font-size: 20px; 414 | font-weight: bold; 415 | color: var(--icons); 416 | } 417 | 418 | ol.sections-name .lesson-details { 419 | display: flex; 420 | flex-basis: 100%; 421 | flex: 1; 422 | background: var(--bg-lesson); 423 | position: relative; 424 | } 425 | 426 | .details-bg { 427 | --corner-fill: var(--corner-inactive); 428 | transition: fill 0.25s; 429 | width: 10%; 430 | height: 0; 431 | padding-bottom: 10%; 432 | background-size: cover; 433 | background-repeat: no-repeat; 434 | position: absolute; 435 | top: 0; 436 | right: 0; 437 | } 438 | 439 | .details-bg > svg { 440 | width: 100%; 441 | height: auto; 442 | } 443 | 444 | .details-bg > svg path { 445 | transition: fill 0.25s; 446 | } 447 | 448 | .lesson-details:hover .details-bg, 449 | .lesson-container .details-bg { 450 | --corner-fill: var(--corner-active); 451 | } 452 | 453 | @media (min-width: 1000px) { 454 | ol.sections-name > li::before { 455 | border-bottom-left-radius: 5px; 456 | border-top-left-radius: 5px; 457 | } 458 | ol.sections-name .lesson-details { 459 | border-bottom-right-radius: 5px; 460 | border-top-right-radius: 5px; 461 | } 462 | } 463 | 464 | @media (max-width: 600px) { 465 | .lesson-container { 466 | padding: 2px; 467 | } 468 | } 469 | 470 | .lesson-details h3 { 471 | font-size: 22px; 472 | border-bottom: 1px solid var(--less); 473 | padding-bottom: 10px; 474 | display: inline-block; 475 | font-weight: bold; 476 | margin-bottom: 20px; 477 | } 478 | 479 | .lesson-links { 480 | margin-top: 45px; 481 | margin-bottom: 80px; 482 | } 483 | 484 | .lesson-links a { 485 | border-radius: 10px; 486 | background: var(--nav-buttons); 487 | color: var(--nav-buttons-text); 488 | padding: 15px 20px; 489 | display: inline-block; 490 | display: flex; 491 | justify-content: center; 492 | align-items: center; 493 | } 494 | 495 | .lesson-links a.prev { 496 | padding-left: 10px; 497 | } 498 | 499 | .lesson-links a.next { 500 | padding-right: 10px; 501 | } 502 | 503 | .lesson-links a:hover { 504 | background: #152837; 505 | text-decoration: none; 506 | } 507 | 508 | .lesson-links .arrow { 509 | font-size: 24px; 510 | line-height: 24px; 511 | padding: 0px 5px; 512 | } 513 | -------------------------------------------------------------------------------- /styles/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | padding: 50px 15px; 4 | background-color: var(--primary); 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | color: var(--text-footer); 9 | } 10 | 11 | .socials { 12 | display: flex; 13 | align-items: center; 14 | max-width: 900px; 15 | width: 100%; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .social { 21 | display: inline-block; 22 | list-style: none; 23 | margin-right: 40px; 24 | } 25 | 26 | .social img:hover { 27 | opacity: 0.4; 28 | } 29 | 30 | .social img { 31 | transition: opacity 0.25s; 32 | width: 30px; 33 | } 34 | 35 | .terms { 36 | font-size: 10px; 37 | } 38 | 39 | .terms p { 40 | margin: 3px; 41 | } 42 | 43 | .footer a { 44 | color: inherit; 45 | text-decoration: underline; 46 | } 47 | 48 | .social svg { 49 | transition: opacity 0.25s; 50 | } 51 | 52 | .social svg:hover { 53 | opacity: 0.4; 54 | } 55 | -------------------------------------------------------------------------------- /styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #dfa067; 3 | --secondary: #93aca7; 4 | --highlight: #224159; 5 | 6 | --text-header: var(--primary); 7 | --text-main-headers: var(--highlight); 8 | --text-links: #007bff; 9 | --text-footer: #333; 10 | 11 | --bg-main: white; 12 | --bg-dots: var(--highlight); 13 | --bg-lesson: white; 14 | 15 | --nav-buttons: var(--highlight); 16 | --nav-buttons-text: white; 17 | 18 | --corner-active: var(--highlight); 19 | --corner-inactive: #f4f4f4; 20 | --icons: var(--highlight); 21 | 22 | --emphasized-bg: #dce8ff; 23 | --emphasized-border: #aab6d2; 24 | } 25 | --------------------------------------------------------------------------------