├── docs ├── _config.yml ├── img │ ├── julia.png │ ├── naome.png │ ├── chapters_img.png │ ├── chapter_layout.png │ └── multi-q-exercise.png └── index.md ├── binder └── requirements.txt ├── static ├── icon.png ├── profile.jpg ├── social.jpg ├── icon_check.svg ├── icon_slides.svg └── logo.svg ├── src ├── context.js ├── styles │ ├── chapter.module.sass │ ├── link.module.sass │ ├── hint.module.sass │ ├── choice.module.sass │ ├── typography.module.sass │ ├── index.module.sass │ ├── code.module.sass │ ├── layout.module.sass │ ├── button.module.sass │ ├── exercise.module.sass │ ├── slides.module.sass │ ├── index.sass │ └── reveal.css ├── components │ ├── typography.js │ ├── hint.js │ ├── button.js │ ├── link.js │ ├── choice.js │ ├── exercise.js │ ├── seo.js │ ├── slides.js │ ├── layout.js │ ├── code.js │ └── juniper.js ├── markdown.js ├── templates │ └── chapter.js └── pages │ └── index.js ├── chapters ├── chapter2.md └── chapter1.md ├── docker-compose.yml ├── exercises ├── exc_01_03.py ├── solution_01_03.py ├── test_01_03.py └── bookquotes.json ├── gatsby-browser.js ├── dockerfile ├── .prettierrc ├── slides └── chapter1_01_introduction.md ├── LICENSE ├── .gitignore ├── meta.json ├── theme.sass ├── package.json ├── gatsby-node.js ├── gatsby-config.js └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /binder/requirements.txt: -------------------------------------------------------------------------------- 1 | wasabi>=0.2.1,<1.1.0 2 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/static/icon.png -------------------------------------------------------------------------------- /docs/img/julia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/docs/img/julia.png -------------------------------------------------------------------------------- /docs/img/naome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/docs/img/naome.png -------------------------------------------------------------------------------- /static/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/static/profile.jpg -------------------------------------------------------------------------------- /static/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/static/social.jpg -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const ChapterContext = React.createContext() 4 | -------------------------------------------------------------------------------- /docs/img/chapters_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/docs/img/chapters_img.png -------------------------------------------------------------------------------- /docs/img/chapter_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/docs/img/chapter_layout.png -------------------------------------------------------------------------------- /docs/img/multi-q-exercise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ines/course-starter-python/HEAD/docs/img/multi-q-exercise.png -------------------------------------------------------------------------------- /src/styles/chapter.module.sass: -------------------------------------------------------------------------------- 1 | .pagination 2 | max-width: 100% 3 | width: var(--width-container) 4 | margin: 4rem auto 0 5 | display: flex 6 | justify-content: space-between 7 | -------------------------------------------------------------------------------- /static/icon_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /chapters/chapter2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chapter 2: More stuff' 3 | description: 4 | 'This chapter will teach you even more stuff and help you learn some new 5 | concepts.' 6 | prev: /chapter1 7 | next: null 8 | type: chapter 9 | id: 2 10 | --- 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | gatsby: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | working_dir: /app 8 | command: gatsby develop -H 0.0.0.0 9 | ports: 10 | - "8000:8000" 11 | volumes: 12 | - .:/app 13 | - /app/node_modules/ 14 | -------------------------------------------------------------------------------- /exercises/exc_01_03.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # This code will run relative to the root of the repo, so we can load files 4 | with open("exercises/bookquotes.json") as f: 5 | DATA = json.loads(f.read()) 6 | 7 | # Print the first record in the DATA 8 | print(___[____]) 9 | 10 | # Assign the length of DATA to some_var 11 | some_var = ___ 12 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import python from 'codemirror/mode/python/python' // eslint-disable-line no-unused-vars 2 | 3 | // This doesn't have to be here – but if we do import Juniper here, it's already 4 | // preloaded and cached when we dynamically import it in code.js. 5 | import Juniper from './src/components/juniper' // eslint-disable-line no-unused-vars 6 | -------------------------------------------------------------------------------- /exercises/solution_01_03.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # This code will run relative to the root of the repo, so we can load files 4 | with open("exercises/bookquotes.json") as f: 5 | DATA = json.loads(f.read()) 6 | 7 | # Print the first record in the DATA 8 | print(DATA[0]) 9 | 10 | # Assign the length of DATA to some_var 11 | some_var = len(DATA) 12 | -------------------------------------------------------------------------------- /src/styles/link.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | cursor: pointer 3 | border-bottom: 1px solid var(--color-theme) 4 | 5 | &:hover 6 | border-bottom-color: var(--color-front) 7 | 8 | .secondary 9 | border-bottom-color: var(--color-subtle-dark) 10 | 11 | &:hover 12 | border-bottom-color: var(--color-front) 13 | 14 | .hidden 15 | border-bottom: 0 16 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | # Docker file for running Gatsby without installing node version 10 or Gatsby. 2 | # Attribution: https://stackoverflow.com/questions/57405792/gatsby-not-rebuilding-whenever-mounted-code-changes 3 | # Hayley Boyce (kinda not really), February 6th, 2020 4 | 5 | FROM node:10 6 | 7 | # Add the package.json file and build the node_modules folder 8 | WORKDIR /app 9 | COPY ./package*.json ./ 10 | RUN mkdir node_modules 11 | RUN npm install --g gatsby-cli 12 | RUN npm install -------------------------------------------------------------------------------- /exercises/test_01_03.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | # Here we can either check objects created in the solution code, or the 3 | # string value of the solution, available as __solution__. A helper for 4 | # printing formatted messages is available as __msg__. See the testTemplate 5 | # in the meta.json for details. 6 | 7 | # If an assertion fails, the message will be displayed 8 | assert "print(DATA[0])" in __solution__, "Are you printing the first record?" 9 | assert some_var == len(DATA), "Are you getting the correct length?" 10 | 11 | __msg__.good("Well done!") 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "printWidth": 100, 7 | "overrides": [ 8 | { 9 | "files": "*.sass", 10 | "options": { 11 | "printWidth": 999 12 | } 13 | }, 14 | { 15 | "files": "*.md", 16 | "options": { 17 | "tabWidth": 2, 18 | "printWidth": 80, 19 | "proseWrap": "always", 20 | "htmlWhitespaceSensitivity": "strict" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/typography.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from '../styles/typography.module.sass' 4 | 5 | export const H3 = ({ children }) =>

{children}

6 | export const Hr = () =>
7 | export const InlineCode = ({ children }) => {children} 8 | 9 | export const Ol = ({ children }) =>
    {children}
10 | export const Ul = ({ children }) => 11 | export const Li = ({ children }) =>
  • {children}
  • 12 | -------------------------------------------------------------------------------- /static/icon_slides.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/styles/hint.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | width: 100% 3 | 4 | .actions 5 | padding-left: 2rem 6 | 7 | .label 8 | cursor: pointer 9 | background: var(--color-subtle-light) 10 | border-width: 0 1px 1px 1px 11 | border-style: solid 12 | border-color: var(--color-subtle-medium) 13 | padding: 0.5rem 1rem 0.5rem 14 | border-bottom-left-radius: var(--border-radius) 15 | border-bottom-right-radius: var(--border-radius) 16 | font-family: var(--font-primary) 17 | font-size: var(--font-size-sm) 18 | font-weight: bold 19 | margin-right: 1rem 20 | 21 | .content 22 | background: var(--color-subtle-light) 23 | padding: 1rem 2rem 24 | font-size: var(--font-size-sm) 25 | border-bottom: 1px solid var(--color-subtle-medium) 26 | 27 | p, ol, ul 28 | margin-bottom: 0 29 | -------------------------------------------------------------------------------- /src/markdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import rehypeReact from 'rehype-react' 3 | 4 | import Exercise from './components/exercise' 5 | import CodeBlock from './components/code' 6 | import { Link } from './components/link' 7 | import Slides from './components/slides' 8 | import Choice, { Option } from './components/choice' 9 | import { H3, Hr, Ol, Ul, Li, InlineCode } from './components/typography' 10 | 11 | export const renderAst = new rehypeReact({ 12 | createElement: React.createElement, 13 | components: { 14 | exercise: Exercise, 15 | slides: Slides, 16 | codeblock: CodeBlock, 17 | choice: Choice, 18 | opt: Option, 19 | a: Link, 20 | hr: Hr, 21 | h3: H3, 22 | ol: Ol, 23 | ul: Ul, 24 | li: Li, 25 | code: InlineCode, 26 | }, 27 | }).Compiler 28 | -------------------------------------------------------------------------------- /src/styles/choice.module.sass: -------------------------------------------------------------------------------- 1 | .option 2 | margin-bottom: 0.5rem 3 | margin-left: 1rem 4 | 5 | .input 6 | display: none 7 | 8 | .label 9 | cursor: pointer 10 | display: flex 11 | align-items: flex-start 12 | 13 | &:before 14 | content: "" 15 | flex: 0 0 20px 16 | width: 20px 17 | height: 20px 18 | display: inline-block 19 | border: 2px solid var(--color-theme) 20 | border-radius: 50% 21 | margin-right: 1rem 22 | margin-top: 0.5rem 23 | 24 | input[type="radio"]:checked + &:before 25 | border-width: 7px 26 | 27 | .answer 28 | padding: 2rem 29 | border-radius: var(--border-radius) 30 | background: var(--color-incorrect-light) 31 | margin-bottom: 2rem 32 | 33 | p 34 | margin-bottom: 0 35 | display: inline 36 | 37 | .correct 38 | background: var(--color-correct-light) 39 | 40 | .answer-label 41 | color: var(--color-incorrect-dark) 42 | 43 | .answer-label-correct 44 | color: var(--color-correct-dark) 45 | -------------------------------------------------------------------------------- /src/styles/typography.module.sass: -------------------------------------------------------------------------------- 1 | .h3 2 | font-size: var(--font-size-md) 3 | font-weight: bold 4 | margin-bottom: 2rem 5 | 6 | .hr 7 | border: 0 8 | height: 0 9 | margin-top: 7rem 10 | margin-bottom: 7rem 11 | 12 | .ul, 13 | .ol 14 | margin-bottom: 3rem 15 | 16 | .ol 17 | list-style: none 18 | counter-reset: ul 19 | 20 | .li 21 | margin-left: 0.5rem 22 | 23 | &:before 24 | margin-right: 1rem 25 | font-weight: bold 26 | counter-increment: ul 27 | content: counter(ul) ". " 28 | 29 | .li 30 | margin-left: 2rem 31 | 32 | .code 33 | background: var(--color-background-code) 34 | padding: 2px 5px 35 | border-radius: var(--border-radius) 36 | 37 | pre & 38 | background: var(--color-subtle-light) 39 | 40 | \:global 41 | pre code 42 | background: var(--color-subtle-light) 43 | 44 | p code, li code 45 | background: var(--color-background-code) 46 | padding: 2px 5px 47 | border-radius: var(--border-radius) 48 | -------------------------------------------------------------------------------- /slides/chapter1_01_introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: slides 3 | --- 4 | 5 | # Introduction 6 | 7 | Notes: Text at the end of a slide prefixed like this will be displayed as 8 | speaker notes on the side. Slides can be separated with a divider: ---. 9 | 10 | --- 11 | 12 | # This is a slide 13 | 14 | ```python 15 | # Print something 16 | print("Hello world") 17 | ``` 18 | 19 | ```out 20 | Hello world 21 | ``` 22 | 23 | - Slides can have code, bullet points, tables and pretty much all other Markdown 24 | elements. 25 | - This is another bullet point. 26 | 27 | This image is in /static 28 | 29 | Notes: Some more notes go here 30 | 31 | --- 32 | 33 | # Let's practice! 34 | 35 | Notes: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique 36 | libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non 37 | commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat 38 | semper urna, in accumsan sapien dui ac mi. Pellentesque felis lorem, semper nec 39 | velit nec, consectetur placerat enim. 40 | -------------------------------------------------------------------------------- /src/styles/index.module.sass: -------------------------------------------------------------------------------- 1 | .logo 2 | width: 300px 3 | height: auto 4 | max-width: 100% 5 | margin: 0 auto 6rem 6 | display: block 7 | color: var(--color-theme) 8 | 9 | .subtitle 10 | font-family: var(--font-display) 11 | width: 600px 12 | height: auto 13 | max-width: 100% 14 | margin: 0 auto 1rem 15 | display: block 16 | text-align: center 17 | 18 | .introduction 19 | width: var(--width-container) 20 | max-width: 100% 21 | padding: 1rem 22 | margin: 0 auto 23 | display: block 24 | text-align: left 25 | 26 | .chapter 27 | width: var(--width-container) 28 | max-width: 100% 29 | margin: 0 auto 30 | padding: 2rem 31 | border: 1px solid var(--color-subtle-medium) 32 | border-radius: var(--border-radius) 33 | margin-bottom: 2rem 34 | 35 | .chapter-title 36 | font-family: var(--font-display) 37 | font-size: var(--font-size-lg) 38 | margin-bottom: 1.5rem 39 | 40 | .chapter-desc 41 | font-size: var(--font-size-sm) 42 | color: var(--color-subtle-dark) 43 | margin-bottom: 0 44 | -------------------------------------------------------------------------------- /src/components/hint.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | 3 | import classes from '../styles/hint.module.sass' 4 | 5 | export const Hint = ({ expanded = false, actions = [], children }) => { 6 | const [isExpanded, setIsExpanded] = useState(expanded) 7 | const handleExpand = useCallback(() => setIsExpanded(!isExpanded), [isExpanded]) 8 | return ( 9 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2019 Ines Montani 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | # gatsby files 60 | .cache/ 61 | public 62 | 63 | # Mac files 64 | .DS_Store 65 | 66 | # Yarn 67 | yarn-error.log 68 | .pnp/ 69 | .pnp.js 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | -------------------------------------------------------------------------------- /exercises/bookquotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin.", 4 | { "author": "Franz Kafka", "book": "Metamorphosis" } 5 | ], 6 | [ 7 | "I know not all that may be coming, but be it what it will, I'll go to it laughing.", 8 | { "author": "Herman Melville", "book": "Moby-Dick or, The Whale" } 9 | ], 10 | [ 11 | "It was the best of times, it was the worst of times.", 12 | { "author": "Charles Dickens", "book": "A Tale of Two Cities" } 13 | ], 14 | [ 15 | "The only people for me are the mad ones, the ones who are mad to live, mad to talk, mad to be saved, desirous of everything at the same time, the ones who never yawn or say a commonplace thing, but burn, burn, burn like fabulous yellow roman candles exploding like spiders across the stars.", 16 | { "author": "Jack Kerouac", "book": "On the Road" } 17 | ], 18 | [ 19 | "It was a bright cold day in April, and the clocks were striking thirteen.", 20 | { "author": "George Orwell", "book": "1984" } 21 | ], 22 | [ 23 | "Nowadays people know the price of everything and the value of nothing.", 24 | { "author": "Oscar Wilde", "book": "The Picture Of Dorian Gray" } 25 | ] 26 | ] 27 | -------------------------------------------------------------------------------- /chapters/chapter1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Chapter 1: Getting started' 3 | description: 4 | 'This chapter will teach you about many cool things and introduce you to the 5 | most important concepts of the course.' 6 | prev: null 7 | next: /chapter2 8 | type: chapter 9 | id: 1 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Let's ask some questions about the slides. Whats the correct answer? 22 | 23 | 24 | 25 | 26 | This is not the correct answer. 27 | 28 | 29 | 30 | 31 | 32 | Good job! 33 | 34 | 35 | 36 | 37 | 38 | This is not correct either. 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | This is a code exercise. The content can be formatted in simple Markdown – so 48 | you can have **bold text**, `code` or [links](https://spacy.io) or lists, like 49 | the one for the instructions below. 50 | 51 | - These are instructions and they can have bullet points. 52 | - The code block below will look for the files `exc_01_03`, `solution_01_03` and 53 | `test_01_03` in `/exercises`. 54 | 55 | 56 | 57 | This is a hint. 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/styles/code.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | margin-left: -2rem 3 | width: calc(100% + 4rem) 4 | margin-bottom: 5rem 5 | 6 | .cell 7 | background: var(--syntax-background) 8 | border-top: 1px solid var(--color-subtle-medium) 9 | 10 | & > div:first-child 11 | padding: 1.5rem 2rem 1rem 12 | position: relative 13 | 14 | &:before 15 | content: "Input" 16 | display: inline-block 17 | border-radius: var(--border-radius) 18 | color: var(--syntax-background) 19 | background: var(--syntax-text) 20 | padding: 0 0.5rem 21 | position: absolute 22 | top: 1rem 23 | right: 1.5rem 24 | font-size: var(--font-size-xs) 25 | font-weight: bold 26 | text-transform: uppercase 27 | z-index: 5 28 | 29 | .output 30 | padding: 2rem 9rem 2rem 3rem 31 | background: var(--syntax-text) 32 | color: var(--syntax-background) 33 | position: relative 34 | 35 | &:before 36 | content: "Output" 37 | display: inline-block 38 | border-radius: var(--border-radius) 39 | background: var(--syntax-background) 40 | color: var(--syntax-text) 41 | padding: 0 0.5rem 42 | position: absolute 43 | top: 1rem 44 | right: 1.5rem 45 | font-size: var(--font-size-xs) 46 | font-weight: bold 47 | text-transform: uppercase 48 | -------------------------------------------------------------------------------- /src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | import IconCheck from '../../static/icon_check.svg' 5 | import classes from '../styles/button.module.sass' 6 | 7 | export const Button = ({ Component = 'button', children, onClick, variant, small, className }) => { 8 | const buttonClassNames = classNames(classes.root, className, { 9 | [classes.primary]: variant === 'primary', 10 | [classes.secondary]: variant === 'secondary', 11 | [classes.small]: !!small, 12 | }) 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | 20 | export const CompleteButton = ({ completed, toggleComplete, small = true }) => { 21 | const buttonClassNames = classNames({ 22 | [classes.completeInactive]: !completed, 23 | [classes.completeActive]: completed, 24 | }) 25 | return ( 26 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link as GatsbyLink } from 'gatsby' 4 | import classNames from 'classnames' 5 | 6 | import classes from '../styles/link.module.sass' 7 | 8 | export const Link = ({ children, to, href, onClick, variant, hidden, className, ...other }) => { 9 | const dest = to || href 10 | const external = /(http(s?)):\/\//gi.test(dest) 11 | const linkClassNames = classNames(classes.root, className, { 12 | [classes.hidden]: hidden, 13 | [classes.secondary]: variant === 'secondary', 14 | }) 15 | 16 | if (!external) { 17 | if ((dest && /^#/.test(dest)) || onClick) { 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | return ( 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | return ( 31 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | Link.propTypes = { 44 | children: PropTypes.node.isRequired, 45 | to: PropTypes.string, 46 | href: PropTypes.string, 47 | onClick: PropTypes.func, 48 | variant: PropTypes.oneOf(['secondary', null]), 49 | hidden: PropTypes.bool, 50 | className: PropTypes.string, 51 | } 52 | -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "courseId": "course-starter-python", 3 | "title": "My cool online course", 4 | "slogan": "A free online course", 5 | "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat semper urna, in accumsan sapien dui ac mi. Pellentesque felis lorem, semper nec velit nec, consectetur placerat enim.", 6 | "bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique libero at est congue, sed vestibulum tortor laoreet. Aenean egestas massa non commodo consequat. Curabitur faucibus, sapien vitae euismod imperdiet, arcu erat semper urna.", 7 | "siteUrl": "https://course-starter-python.netlify.com", 8 | "twitter": "spacy_io", 9 | "fonts": "IBM+Plex+Mono:500|IBM+Plex+Sans:700|Lato:400,400i,700,700i", 10 | "testTemplate": "from wasabi import Printer\n__msg__ = Printer()\n__solution__ = \"\"\"${solution}\"\"\"\n\n${solution}\n\n${test}\n\ntry:\n test()\nexcept AssertionError as e:\n __msg__.fail(e)", 11 | "juniper": { 12 | "repo": "ines/course-starter-python", 13 | "branch": "binder", 14 | "lang": "python", 15 | "kernelType": "python3", 16 | "debug": false 17 | }, 18 | "showProfileImage": true, 19 | "footerLinks": [ 20 | { "text": "Website", "url": "https://spacy.io" }, 21 | { "text": "Source", "url": "https://github.com/ines/course-starter-python" }, 22 | { "text": "Built with ♥", "url": "https://github.com/ines/course-starter-python" } 23 | ], 24 | "theme": "#de7878" 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/layout.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | width: 100% 3 | 4 | .content 5 | margin-bottom: 4rem 6 | padding: 6rem 1rem 1rem 7 | 8 | .logo 9 | color: var(--color-theme) 10 | position: absolute 11 | top: 2rem 12 | left: 2rem 13 | 14 | @media(min-width: 1200px) 15 | .logo 16 | position: fixed 17 | 18 | .header 19 | width: var(--width-container) 20 | max-width: 100% 21 | margin: 5rem auto 22 | 23 | .title 24 | font-size: var(--font-size-xl) 25 | font-family: var(--font-display) 26 | line-height: 1.3 27 | margin-bottom: 2rem 28 | 29 | .footer 30 | background: var(--color-subtle-light) 31 | width: 100% 32 | padding: 3rem 33 | margin-top: 5.5rem 34 | 35 | .footer-content 36 | max-width: 100% 37 | width: var(--width-container) 38 | margin: 0 auto 39 | font-size: var(--font-size-sm) 40 | justify-content: space-between 41 | flex-flow: row wrap 42 | 43 | @media(min-width: 900px) 44 | .footer-content 45 | display: flex 46 | 47 | .footer-section 48 | flex: 0 0 48% 49 | padding: 0 2rem 50 | margin-bottom: 1rem 51 | 52 | .footer-links 53 | flex: 0 0 100% 54 | padding-top: 2rem 55 | font-size: var(--font-size-xs) 56 | text-align: center 57 | color: var(--color-subtle-dark) 58 | 59 | .footer-link 60 | display: inline 61 | 62 | &:not(:last-child) 63 | margin-right: 1rem 64 | 65 | &:after 66 | content: "\b7" 67 | margin-left: 1rem 68 | 69 | .profile 70 | width: var(--size-profile-pic) 71 | height: var(--size-profile-pic) 72 | border-radius: 50% 73 | float: right 74 | margin: 0 0 1rem 1rem 75 | shape-outside: circle() 76 | -------------------------------------------------------------------------------- /src/styles/button.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | border: 0 3 | background: var(--color-button-secondary) 4 | color: var(--color-button-secondary-contrast) 5 | padding: 0.75rem 1rem 6 | font-family: var(--font-primary) 7 | font-size: var(--font-size-sm) 8 | font-weight: bold 9 | border-radius: var(--border-radius) 10 | margin-top: 3rem 11 | margin-bottom: 2.25rem 12 | margin-left: 2.5rem 13 | transition: opacity 0.2s ease 14 | box-shadow: 0 0 3px var(--color-subtle-medium) 15 | 16 | &:hover 17 | opacity: 0.9 18 | box-shadow: none 19 | 20 | .primary 21 | background: var(--color-button-primary) 22 | color: var(--color-button-primary-contrast) 23 | 24 | .secondary 25 | background: var(--color-theme) 26 | color: var(--color-theme-contrast) 27 | 28 | .small 29 | padding: 0.75rem 1rem 30 | margin: 0 31 | font-size: var(--font-size-sm) 32 | 33 | &:not(:last-child) 34 | margin-right: 1rem 35 | 36 | .complete-icon 37 | position: relative 38 | top: 0.15rem 39 | margin-right: 0.5rem 40 | 41 | .complete-inactive 42 | &:hover 43 | opacity: 1 44 | background: var(--color-button-primary) 45 | color: var(--color-button-primary-contrast) 46 | 47 | .complete-active 48 | background: var(--color-button-primary) 49 | color: var(--color-button-primary-contrast) 50 | 51 | &:hover 52 | opacity: 1 53 | background: var(--color-button-danger) 54 | color: var(--color-button-danger-contrast) 55 | 56 | .complete-label 57 | .root:hover & 58 | display: none 59 | 60 | .complete-label-hover 61 | display: none 62 | 63 | .root:hover & 64 | display: inline 65 | -------------------------------------------------------------------------------- /src/styles/exercise.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | width: var(--width-container) 3 | max-width: 100% 4 | padding: 1rem 2rem 5 | border: 1px solid var(--color-subtle-medium) 6 | border-radius: var(--border-radius) 7 | margin: 0 auto 2rem 8 | position: relative 9 | background: var(--color-back) 10 | 11 | .wide 12 | width: 95% 13 | 14 | .expanded 15 | box-shadow: 0 0 10px var(--color-subtle-medium) 16 | 17 | .completed 18 | color: var(--color-subtle-dark) 19 | background: var(--color-subtle-light) 20 | 21 | .title 22 | cursor: pointer 23 | user-select: none 24 | font-size: var(--font-size-lg) 25 | font-family: var(--font-display) 26 | 27 | .id 28 | background: var(--color-theme) 29 | color: var(--color-theme-contrast) 30 | font-size: 0.8em 31 | width: 1.75em 32 | height: 1.75em 33 | display: inline-flex 34 | border-radius: 50% 35 | justify-content: center 36 | align-items: center 37 | margin-right: 1rem 38 | 39 | .id-completed 40 | background: var(--color-button-primary) 41 | color: var(--color-button-primary-contrast) 42 | 43 | .title-expanded 44 | margin-bottom: 3rem 45 | 46 | .icon 47 | float: right 48 | vertical-align: middle 49 | margin-right: 0.5rem 50 | 51 | .content 52 | visibility: hidden 53 | height: 0 54 | opacity: 0 55 | z-index: 0 56 | overflow: hidden 57 | 58 | .footer 59 | width: 100% 60 | text-align: right 61 | margin-top: 1rem 62 | margin-bottom: 1rem 63 | 64 | .button 65 | cursor: pointer 66 | border: 0 67 | background: var(--color-theme) 68 | color: var(--color-theme-contrast) 69 | padding: 0.75rem 1rem 70 | font-family: var(--font-primary) 71 | font-size: var(--font-size-sm) 72 | font-weight: bold 73 | border-radius: var(--border-radius) 74 | -------------------------------------------------------------------------------- /theme.sass: -------------------------------------------------------------------------------- 1 | \:root 2 | --font-primary: Lato, Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' 3 | --font-display: 'IBM Plex Sans', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' 4 | --font-code: 'IBM Plex Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace 5 | 6 | // Font Sizes 7 | --font-size-xs: 1.15rem 8 | --font-size-sm: 1.3rem 9 | --font-size-md: 1.5rem 10 | --font-size-lg: 1.8rem 11 | --font-size-xl: 2.4rem 12 | 13 | // Line heights 14 | --line-height: 1.6 15 | 16 | // Colors 17 | --color-back: #fff 18 | --color-front: #444 19 | --color-theme: #de7878 20 | --color-theme-contrast: #fff 21 | --color-subtle-dark: #777 22 | --color-subtle-medium: #eee 23 | --color-subtle-light: #f9f9f9 24 | --color-background-code: #7a818117 25 | 26 | --color-button-primary: #29c15b 27 | --color-button-primary-contrast: #fff 28 | --color-button-secondary: #cdcdd4 29 | --color-button-secondary-contrast: #686873 30 | --color-button-danger: #d21313 31 | --color-button-danger-contrast: #fff 32 | 33 | --color-correct-light: #e3fde8 34 | --color-correct-dark: #007737 35 | --color-incorrect-light: #fde7e7 36 | --color-incorrect-dark: #d21313 37 | 38 | // Syntax Highlighting 39 | // based on https://github.com/sdras/night-owl-vscode-theme 40 | --syntax-background: #f7f7f7 41 | --syntax-text: #403f53 42 | --syntax-selected-background: #7a81812b 43 | --syntax-comment: #989fb1 44 | --syntax-tag: #994cc3 45 | --syntax-number: #aa0982 46 | --syntax-selector: #994cc3 47 | --syntax-operator: #994cc3 48 | --syntax-function: #4876d6 49 | --syntax-keyword: #994cc3 50 | --syntax-regex: #5ca7e4 51 | 52 | --ansi-green: #12dc55 53 | --ansi-red: #f76464 54 | --ansi-cyan: #00d8ff 55 | 56 | // Sizes 57 | --width-container: 800px 58 | --size-profile-pic: 125px 59 | --border-radius: 5px 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "course-starter-python", 3 | "private": true, 4 | "description": "Starter package to build interactive Python courses", 5 | "version": "0.0.1", 6 | "author": "Ines Montani ", 7 | "dependencies": { 8 | "@illinois/react-use-local-storage": "^1.1.0", 9 | "@jupyterlab/outputarea": "^0.19.1", 10 | "@jupyterlab/rendermime": "^0.19.1", 11 | "@phosphor/widgets": "^1.6.0", 12 | "autoprefixer": "^9.4.7", 13 | "classnames": "^2.2.6", 14 | "codemirror": "^5.43.0", 15 | "gatsby": "^2.1.4", 16 | "gatsby-image": "^2.0.29", 17 | "gatsby-plugin-manifest": "^2.0.17", 18 | "gatsby-plugin-offline": "^2.0.23", 19 | "gatsby-plugin-react-helmet": "^3.0.6", 20 | "gatsby-plugin-react-svg": "^2.1.1", 21 | "gatsby-plugin-sass": "^2.0.10", 22 | "gatsby-plugin-sharp": "^2.0.29", 23 | "gatsby-plugin-sitemap": "^2.0.5", 24 | "gatsby-remark-copy-linked-files": "^2.0.9", 25 | "gatsby-remark-images": "^3.0.4", 26 | "gatsby-remark-prismjs": "^3.2.4", 27 | "gatsby-remark-smartypants": "^2.0.8", 28 | "gatsby-remark-unwrap-images": "^1.0.1", 29 | "gatsby-source-filesystem": "^2.0.20", 30 | "gatsby-transformer-remark": "^2.2.5", 31 | "gatsby-transformer-sharp": "^2.1.17", 32 | "juniper-js": "^0.1.0", 33 | "node-sass": "^4.11.0", 34 | "prismjs": "^1.15.0", 35 | "react": "^16.8.2", 36 | "react-dom": "^16.8.2", 37 | "react-helmet": "^5.2.0", 38 | "rehype-react": "^3.1.0", 39 | "remark-react": "^5.0.1", 40 | "reveal.js": "^3.8.0" 41 | }, 42 | "scripts": { 43 | "build": "gatsby build", 44 | "dev": "gatsby develop", 45 | "lint": "eslint **", 46 | "clear": "rm -rf .cache", 47 | "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"" 48 | }, 49 | "devDependencies": { 50 | "browser-monads": "^1.0.0", 51 | "prettier": "^1.16.4" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/ines/course-starter-python" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/templates/chapter.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { graphql, navigate } from 'gatsby' 3 | import useLocalStorage from '@illinois/react-use-local-storage' 4 | 5 | import { renderAst } from '../markdown' 6 | import { ChapterContext } from '../context' 7 | import Layout from '../components/layout' 8 | import { Button } from '../components/button' 9 | 10 | import classes from '../styles/chapter.module.sass' 11 | 12 | const Template = ({ data }) => { 13 | const { markdownRemark, site } = data 14 | const { courseId } = site.siteMetadata 15 | const { frontmatter, htmlAst } = markdownRemark 16 | const { title, description, prev, next, id } = frontmatter 17 | const [activeExc, setActiveExc] = useState(null) 18 | const [completed, setCompleted] = useLocalStorage(`${courseId}-completed-${id}`, []) 19 | const html = renderAst(htmlAst) 20 | const buttons = [ 21 | { slug: prev, text: '« Previous Chapter' }, 22 | { slug: next, text: 'Next Chapter »' }, 23 | ] 24 | 25 | return ( 26 | 27 | 28 | {html} 29 | 30 |
    31 | {buttons.map(({ slug, text }) => ( 32 |
    33 | {slug && ( 34 | 37 | )} 38 |
    39 | ))} 40 |
    41 |
    42 |
    43 | ) 44 | } 45 | 46 | export default Template 47 | 48 | export const pageQuery = graphql` 49 | query($slug: String!) { 50 | site { 51 | siteMetadata { 52 | courseId 53 | } 54 | } 55 | markdownRemark(fields: { slug: { eq: $slug } }) { 56 | htmlAst 57 | frontmatter { 58 | id 59 | title 60 | description 61 | next 62 | prev 63 | } 64 | } 65 | } 66 | ` 67 | -------------------------------------------------------------------------------- /src/components/choice.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import classNames from 'classnames' 3 | 4 | import { Button } from './button' 5 | import classes from '../styles/choice.module.sass' 6 | 7 | const Choice = ({ id = '0', children = [] }) => { 8 | const [selected, setSelected] = useState(null) 9 | const [answer, setAnswer] = useState(null) 10 | const handleAnswer = useCallback(() => setAnswer(selected), [selected]) 11 | const options = children.filter(child => child !== '\n') 12 | return ( 13 | <> 14 | {options.map(({ key, props }, i) => ( 15 |

    16 | setSelected(i)} 24 | /> 25 |

    31 | ))} 32 | 35 | {options.map(({ key, props }, i) => { 36 | const isCorrect = !!props.correct 37 | return answer === i ? ( 38 |
    42 | 47 | {isCorrect ? "That's correct! " : 'Incorrect. '} 48 | 49 | {props.children} 50 |
    51 | ) : null 52 | })} 53 | 54 | ) 55 | } 56 | 57 | export const Option = ({ children }) => { 58 | return children 59 | } 60 | 61 | export default Choice 62 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Layout from '../components/layout' 5 | import { Link } from '../components/link' 6 | import Logo from '../../static/logo.svg' 7 | 8 | import classes from '../styles/index.module.sass' 9 | 10 | export default ({ data }) => { 11 | const siteMetadata = data.site.siteMetadata 12 | const chapters = data.allMarkdownRemark.edges.map(({ node }) => ({ 13 | slug: node.fields.slug, 14 | title: node.frontmatter.title, 15 | description: node.frontmatter.description, 16 | })) 17 | return ( 18 | 19 | 20 | 21 |
    22 |

    INSERT CATCHY TAG LINE HERE

    23 |
    24 |

    25 | This is a good place to write what your course is about! Dinosaurs? Neural Networks? LSTM? How to do taxes and not get bored? Write it here! 26 |

    27 |
    28 |
    29 | 30 | {chapters.map(({ slug, title, description }) => ( 31 |
    32 |

    33 | 34 | {title} 35 | 36 |

    37 |

    38 | 39 | {description} 40 | 41 |

    42 |
    43 | ))} 44 |
    45 | ) 46 | } 47 | 48 | export const pageQuery = graphql` 49 | { 50 | site { 51 | siteMetadata { 52 | title 53 | } 54 | } 55 | allMarkdownRemark( 56 | sort: { fields: [frontmatter___title], order: ASC } 57 | filter: { frontmatter: { type: { eq: "chapter" } } } 58 | ) { 59 | edges { 60 | node { 61 | fields { 62 | slug 63 | } 64 | frontmatter { 65 | title 66 | description 67 | } 68 | } 69 | } 70 | } 71 | } 72 | ` 73 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { createFilePath } = require('gatsby-source-filesystem') 3 | 4 | const chapterTemplate = path.resolve('src/templates/chapter.js') 5 | 6 | function replacePath(pagePath) { 7 | return pagePath === `/` ? pagePath : pagePath.replace(/\/$/, ``) 8 | } 9 | 10 | async function onCreateNode({ 11 | node, 12 | actions, 13 | getNode, 14 | loadNodeContent, 15 | createNodeId, 16 | createContentDigest, 17 | }) { 18 | const { createNodeField, createNode, createParentChildLink } = actions 19 | if (node.internal.type === 'MarkdownRemark') { 20 | const slug = createFilePath({ node, getNode, basePath: 'chapters', trailingSlash: false }) 21 | createNodeField({ name: 'slug', node, value: slug }) 22 | } else if (node.extension === 'py') { 23 | // Load the contents of the Python file and make it available via GraphQL 24 | // https://www.gatsbyjs.org/docs/creating-a-transformer-plugin/ 25 | const content = await loadNodeContent(node) 26 | const contentDigest = createContentDigest(content) 27 | const id = createNodeId(`${node.id}-code`) 28 | const internal = { type: 'Code', contentDigest } 29 | const codeNode = { 30 | id, 31 | parent: node.id, 32 | children: [], 33 | code: content, 34 | name: node.name, 35 | internal, 36 | } 37 | createNode(codeNode) 38 | createParentChildLink({ parent: node, child: codeNode }) 39 | } 40 | } 41 | 42 | exports.onCreateNode = onCreateNode 43 | 44 | exports.createPages = ({ actions, graphql }) => { 45 | const { createPage } = actions 46 | return graphql(` 47 | { 48 | allMarkdownRemark { 49 | edges { 50 | node { 51 | frontmatter { 52 | title 53 | type 54 | } 55 | fields { 56 | slug 57 | } 58 | } 59 | } 60 | } 61 | } 62 | `).then(result => { 63 | if (result.errors) { 64 | return Promise.reject(result.errors) 65 | } 66 | const posts = result.data.allMarkdownRemark.edges.filter( 67 | ({ node }) => node.frontmatter.type == 'chapter' 68 | ) 69 | posts.forEach(({ node }) => { 70 | createPage({ 71 | path: replacePath(node.fields.slug), 72 | component: chapterTemplate, 73 | context: { slug: node.fields.slug }, 74 | }) 75 | }) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/exercise.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useContext, useEffect } from 'react' 2 | import classNames from 'classnames' 3 | 4 | import { Button, CompleteButton } from './button' 5 | import { ChapterContext } from '../context' 6 | import IconSlides from '../../static/icon_slides.svg' 7 | import classes from '../styles/exercise.module.sass' 8 | 9 | const Exercise = ({ id, title, type, children }) => { 10 | const excRef = useRef() 11 | const excId = parseInt(id) 12 | const { activeExc, setActiveExc, completed, setCompleted } = useContext(ChapterContext) 13 | const isExpanded = activeExc === excId 14 | const isCompleted = completed.includes(excId) 15 | useEffect(() => { 16 | if (isExpanded && excRef.current) { 17 | excRef.current.scrollIntoView() 18 | } 19 | }, [isExpanded]) 20 | const handleExpand = useCallback(() => setActiveExc(isExpanded ? null : excId), [ 21 | isExpanded, 22 | excId, 23 | ]) 24 | const handleNext = useCallback(() => setActiveExc(excId + 1)) 25 | const handleSetCompleted = useCallback(() => { 26 | const newCompleted = isCompleted 27 | ? completed.filter(v => v !== excId) 28 | : [...completed, excId] 29 | setCompleted(newCompleted) 30 | }, [isCompleted, completed, excId]) 31 | const rootClassNames = classNames(classes.root, { 32 | [classes.expanded]: isExpanded, 33 | [classes.wide]: isExpanded && type === 'slides', 34 | [classes.completed]: !isExpanded && isCompleted, 35 | }) 36 | const titleClassNames = classNames(classes.title, { 37 | [classes.titleExpanded]: isExpanded, 38 | }) 39 | return ( 40 |
    41 |

    42 | 43 | 46 | {excId} 47 | 48 | {title} 49 | 50 | {type === 'slides' && } 51 |

    52 | {isExpanded && ( 53 |
    54 | {children} 55 |
    56 | 60 | 63 |
    64 |
    65 | )} 66 |
    67 | ) 68 | } 69 | 70 | export default Exercise 71 | -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | import { StaticQuery, graphql } from 'gatsby' 4 | 5 | const SEO = ({ title, description }) => ( 6 | { 9 | const lang = 'en' 10 | const siteMetadata = data.site.siteMetadata 11 | const pageTitle = title 12 | ? `${title} · ${siteMetadata.title}` 13 | : `${siteMetadata.title} · ${siteMetadata.slogan}` 14 | const pageDesc = description || siteMetadata.description 15 | const image = `${siteMetadata.siteUrl}/social.jpg` 16 | const meta = [ 17 | { 18 | name: 'description', 19 | content: pageDesc, 20 | }, 21 | { 22 | property: 'og:title', 23 | content: pageTitle, 24 | }, 25 | { 26 | property: 'og:description', 27 | content: pageDesc, 28 | }, 29 | { 30 | property: 'og:type', 31 | content: `website`, 32 | }, 33 | { 34 | property: 'og:site_name', 35 | content: siteMetadata.title, 36 | }, 37 | { 38 | property: 'og:image', 39 | content: image, 40 | }, 41 | { 42 | name: 'twitter:card', 43 | content: 'summary_large_image', 44 | }, 45 | { 46 | name: 'twitter:image', 47 | content: image, 48 | }, 49 | { 50 | name: 'twitter:creator', 51 | content: `@${siteMetadata.twitter}`, 52 | }, 53 | { 54 | name: 'twitter:site', 55 | content: `@${siteMetadata.twitter}`, 56 | }, 57 | { 58 | name: 'twitter:title', 59 | content: pageTitle, 60 | }, 61 | { 62 | name: 'twitter:description', 63 | content: pageDesc, 64 | }, 65 | ] 66 | 67 | return ( 68 | 69 | {siteMetadata.fonts && ( 70 | 74 | )} 75 | 76 | ) 77 | }} 78 | /> 79 | ) 80 | 81 | export default SEO 82 | 83 | const query = graphql` 84 | query DefaultSEOQuery { 85 | site { 86 | siteMetadata { 87 | title 88 | description 89 | slogan 90 | siteUrl 91 | twitter 92 | fonts 93 | } 94 | } 95 | } 96 | ` 97 | -------------------------------------------------------------------------------- /src/styles/slides.module.sass: -------------------------------------------------------------------------------- 1 | .root 2 | position: relative 3 | width: 100% 4 | height: 600px 5 | overflow: hidden 6 | 7 | .slides 8 | text-align: left !important 9 | 10 | \:global 11 | .reveal 12 | h1 13 | border-bottom: 1px solid var(--color-subtle-medium) 14 | 15 | h1, h2, h3, pre, ul, ol 16 | margin-bottom: 3rem 17 | 18 | li 19 | margin-left: 2rem 20 | 21 | img 22 | margin-bottom: 2rem 23 | 24 | img[align] 25 | padding: 2rem 26 | 27 | td 28 | border-bottom: 1px solid var(--color-subtle-medium) 29 | 30 | th 31 | border-bottom: 2px solid var(--color-theme) 32 | 33 | a 34 | color: var(--color-theme) 35 | text-decoration: underline 36 | 37 | section 38 | height: 100% 39 | padding: 1rem 40 | 41 | section:first-child h1, 42 | section:last-child h1 43 | font-size: 4rem 44 | border: 0 45 | text-align: center 46 | width: 100% 47 | height: 100% 48 | display: flex 49 | align-items: center 50 | justify-content: center 51 | 52 | pre 53 | margin-bottom: 1.5rem 54 | 55 | pre code 56 | width: 100% 57 | display: block 58 | padding: 0.5rem 1rem !important 59 | border-radius: var(--border-radius) 60 | background: var(--syntax-background) 61 | color: var(--syntax-text) 62 | position: relative 63 | 64 | pre code.language-out 65 | background: var(--syntax-text) 66 | color: var(--syntax-background) 67 | 68 | &:before 69 | content: "Output" 70 | position: absolute 71 | top: 0.5rem 72 | right: 1rem 73 | font-family: var(--font-primary) 74 | text-transform: uppercase 75 | font-weight: bold 76 | font-size: var(--font-size-xs) 77 | 78 | .reveal.show-notes 79 | max-width: 70% 80 | 81 | .reveal .speaker-notes 82 | background: var(--color-back) 83 | border: 2px solid var(--color-subtle-medium) 84 | color: var(--color-subtle-dark) 85 | font-size: var(--font-size-xs) 86 | font-family: var(--font-primary) 87 | border-radius: var(--border-radius) 88 | width: 40% 89 | margin-left: 2.75% 90 | height: 100% 91 | overflow: auto 92 | 93 | p 94 | margin-bottom: 1.5rem 95 | 96 | &:before 97 | content: "Script" 98 | font-weight: bold 99 | color: inherit 100 | opacity: 1 101 | 102 | @media(max-width: 1024px) 103 | .reveal.show-notes 104 | max-width: 100% 105 | 106 | .reveal.show-notes .speaker-notes 107 | width: 100% 108 | margin-left: 0 109 | top: 125% 110 | overflow: auto 111 | 112 | @media(max-width: 600px) 113 | .reveal.show-notes .speaker-notes 114 | height: auto 115 | top: 100% 116 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const meta = require('./meta.json') 2 | const autoprefixer = require('autoprefixer') 3 | 4 | module.exports = { 5 | siteMetadata: meta, 6 | plugins: [ 7 | { 8 | resolve: `gatsby-plugin-sass`, 9 | options: { 10 | indentedSyntax: true, 11 | postCssPlugins: [autoprefixer()], 12 | cssLoaderOptions: { 13 | localIdentName: 14 | process.env.NODE_ENV == 'development' 15 | ? '[name]-[local]-[hash:8]' 16 | : '[hash:8]', 17 | }, 18 | }, 19 | }, 20 | `gatsby-plugin-react-helmet`, 21 | { 22 | resolve: `gatsby-source-filesystem`, 23 | options: { 24 | name: `chapters`, 25 | path: `${__dirname}/chapters`, 26 | }, 27 | }, 28 | { 29 | resolve: `gatsby-source-filesystem`, 30 | options: { 31 | name: `slides`, 32 | path: `${__dirname}/slides`, 33 | }, 34 | }, 35 | { 36 | resolve: `gatsby-source-filesystem`, 37 | options: { 38 | name: `exercises`, 39 | path: `${__dirname}/exercises`, 40 | }, 41 | }, 42 | { 43 | resolve: 'gatsby-plugin-react-svg', 44 | options: { 45 | rule: { 46 | include: /static/, 47 | }, 48 | }, 49 | }, 50 | { 51 | resolve: `gatsby-transformer-remark`, 52 | options: { 53 | plugins: [ 54 | `gatsby-remark-copy-linked-files`, 55 | { 56 | resolve: `gatsby-remark-prismjs`, 57 | options: { 58 | noInlineHighlight: true, 59 | }, 60 | }, 61 | { 62 | resolve: `gatsby-remark-smartypants`, 63 | options: { 64 | dashes: 'oldschool', 65 | }, 66 | }, 67 | { 68 | resolve: `gatsby-remark-images`, 69 | options: { 70 | maxWidth: 790, 71 | linkImagesToOriginal: true, 72 | sizeByPixelDensity: false, 73 | showCaptions: true, 74 | quality: 80, 75 | withWebp: { quality: 80 }, 76 | }, 77 | }, 78 | `gatsby-remark-unwrap-images`, 79 | ], 80 | }, 81 | }, 82 | `gatsby-transformer-sharp`, 83 | `gatsby-plugin-sharp`, 84 | `gatsby-plugin-sitemap`, 85 | { 86 | resolve: `gatsby-plugin-manifest`, 87 | options: { 88 | name: meta.title, 89 | short_name: meta.title, 90 | start_url: `/`, 91 | background_color: meta.theme, 92 | theme_color: meta.theme, 93 | display: `minimal-ui`, 94 | icon: `static/icon.png`, 95 | }, 96 | }, 97 | `gatsby-plugin-offline`, 98 | ], 99 | } 100 | -------------------------------------------------------------------------------- /src/components/slides.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StaticQuery, graphql } from 'gatsby' 3 | import Marked from 'reveal.js/plugin/markdown/marked.js' 4 | import classNames from 'classnames' 5 | 6 | import '../styles/reveal.css' 7 | import classes from '../styles/slides.module.sass' 8 | 9 | function getFiles({ allMarkdownRemark }) { 10 | return Object.assign( 11 | {}, 12 | ...allMarkdownRemark.edges.map(({ node }) => ({ 13 | [node.fields.slug.replace('/', '')]: node.rawMarkdownBody, 14 | })) 15 | ) 16 | } 17 | 18 | function getSlideContent(data, source) { 19 | const files = getFiles(data) 20 | const file = files[source] || '' 21 | return file.split('\n---\n').map(f => f.trim()) 22 | } 23 | 24 | class Slides extends React.Component { 25 | componentDidMount() { 26 | import('reveal.js').then(({ default: Reveal }) => { 27 | window.Reveal = Reveal 28 | window.marked = Marked 29 | import('reveal.js/plugin/markdown/markdown.js').then(({ RevealMarkdown }) => { 30 | RevealMarkdown.init() 31 | Reveal.initialize({ 32 | center: false, 33 | progress: false, 34 | showNotes: true, 35 | controls: true, 36 | width: '100%', 37 | height: 600, 38 | minScale: 0.75, 39 | maxScale: 1, 40 | }) 41 | }) 42 | }) 43 | } 44 | 45 | componentWillUnmount() { 46 | // Work around default reveal.js behaviour that doesn't allow 47 | // re-initialization and clashes with React 48 | delete window.Reveal 49 | delete window.marked 50 | delete require.cache[require.resolve('reveal.js')] 51 | delete require.cache[require.resolve('reveal.js/plugin/markdown/markdown.js')] 52 | } 53 | 54 | render() { 55 | const { source } = this.props 56 | const revealClassNames = classNames('reveal', 'show-notes', classes.reveal) 57 | const slideClassNames = classNames('slides', classes.slides) 58 | 59 | return ( 60 |
    61 |
    62 | { 80 | const content = getSlideContent(data, source) 81 | return ( 82 |
    83 | {content.map((markdown, i) => ( 84 |
    89 |