├── .env ├── .env.development ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── components ├── Layout │ ├── Footer.js │ ├── Header.css │ ├── Header.js │ └── index.js ├── Notes │ ├── Notes.css │ └── index.js ├── Rating.js ├── Session.css └── Session.js ├── hocs ├── withToken.js └── withUser.js ├── mock-api ├── db.json ├── routes.json └── users.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── _error.js ├── about.js ├── auth │ ├── callback.js │ ├── check-email.js │ ├── error.js │ └── index.js ├── contact.js ├── explode.js ├── index-class.js ├── index.js └── session.js ├── routes ├── .eslintrc.js ├── auth.js ├── cors.js ├── db │ ├── index.js │ ├── user.js │ └── user.test.js ├── index.js ├── strategy │ └── github.js └── user.js ├── server.js └── static ├── nprogress.css ├── remy.jpg └── styles.css /.env: -------------------------------------------------------------------------------- 1 | SHOW_SPEAKER=0 2 | PORT=3000 3 | API=https://next-workshop.now.sh 4 | SESSION_SECRET=lwekjfkjlkz2134 5 | HOST=https://next-workshop.now.sh 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | SHOW_SPEAKERS=1 2 | GITHUB_CLIENT_ID=35d9015d3692581de547 3 | GITHUB_SECRET=4d70765a608d1fc5ed1cb1358fa662f6ae3c8247 4 | GITHUB_CALLBACK= 5 | SHOW_SPEAKER=1 6 | PORT=3000 7 | #API=https://next-workshop.now.sh 8 | API=http://localhost:3001 9 | SESSION_SECRET=2094803289saljdlk 10 | HOST=http://localhost:3000 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | out 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "commonjs": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "experimentalObjectRestSpread": true, 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/prop-types": 0, 19 | "react/jsx-uses-vars": [2] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next 3 | /tmp 4 | /out 5 | /.vscode 6 | -------------------------------------------------------------------------------- /components/Layout/Footer.js: -------------------------------------------------------------------------------- 1 | export default () => ; 2 | -------------------------------------------------------------------------------- /components/Layout/Header.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | /* display: flex; */ 3 | } 4 | 5 | .Header nav { 6 | /* flex-grow: 1; */ 7 | } 8 | 9 | .Header .UserNav { 10 | display: inline-block; 11 | float: right; 12 | /* padding: 10px; */ 13 | position: relative; 14 | /* color: white; */ 15 | padding-right: 42px; 16 | } 17 | 18 | .Header img { 19 | position: absolute; 20 | top: 5px; 21 | right: 0; 22 | width: 32px; 23 | height: 32px; 24 | border-radius: 32px; 25 | } 26 | -------------------------------------------------------------------------------- /components/Layout/Header.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import './Header.css'; 4 | 5 | export default ({ user = {} }) => ( 6 |
7 | 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Header from './Header'; 3 | import Footer from './Footer'; 4 | import NProgress from 'nprogress'; 5 | import Router from 'next/router'; 6 | 7 | Router.onRouteChangeStart = url => { 8 | console.log(`Loading: ${url}`); 9 | NProgress.start(); 10 | }; 11 | Router.onRouteChangeComplete = () => NProgress.done(); 12 | Router.onRouteChangeError = () => NProgress.done(); 13 | 14 | export default ({ children, title = 'Nextconf Schedule', ...props }) => ( 15 |
16 | 17 | {title} 18 | 19 | 20 |
21 |
{children}
22 |
24 | ); 25 | -------------------------------------------------------------------------------- /components/Notes/Notes.css: -------------------------------------------------------------------------------- 1 | .Notes { 2 | display: block; 3 | } 4 | 5 | .CodeMirror, 6 | textarea { 7 | width: 100%; 8 | font-family: 'ubuntu mono', monospace; 9 | font-size: 13px; 10 | min-height: 100px; 11 | height: auto; 12 | border: 2px solid #ccc; 13 | padding: 10px; 14 | border-radius: 2px; 15 | margin-bottom: 10px; 16 | } 17 | -------------------------------------------------------------------------------- /components/Notes/index.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'react-codemirror'; 2 | require('codemirror/mode/markdown/markdown'); 3 | 4 | const Notes = props => ( 5 | 10 | ); 11 | 12 | export default Notes; 13 | -------------------------------------------------------------------------------- /components/Rating.js: -------------------------------------------------------------------------------- 1 | export default ({ value: selected, name = `rating` }) => { 2 | return selected === undefined ? null : ( 3 | 4 | {Array.from({ length: 5 }).reduce((acc, curr, i) => { 5 | acc.push( 6 | 13 | ); 14 | acc.push( 15 | 22 | ); 23 | 24 | return acc; 25 | }, [])} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/Session.css: -------------------------------------------------------------------------------- 1 | .Speaker a { 2 | font-weight: 800; 3 | } 4 | -------------------------------------------------------------------------------- /components/Session.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import Link from 'next/link'; 3 | import Rating from './Rating'; 4 | import dynamic from 'next/dynamic'; 5 | import Head from 'next/head'; 6 | import './Session.css'; 7 | 8 | const Notes = dynamic({ 9 | modules: () => { 10 | const components = { 11 | css1: import('codemirror/lib/codemirror.css'), 12 | css2: import('./Notes/Notes.css'), 13 | Notes: import('./Notes'), 14 | }; 15 | 16 | return components; 17 | }, 18 | render: (props, { Notes, css1, css2 }) => { 19 | return ( 20 | 21 | 22 | 27 | 28 | 29 | 30 | ); 31 | }, 32 | loading: () =>

Loading notes…

, 33 | ssr: false, 34 | }); 35 | 36 | const Speaker = ({ speaker, twitter }) => 37 | speaker ? ( 38 |

39 | {speaker} / @{twitter} 40 |

41 | ) : null; 42 | 43 | export default ({ 44 | title, 45 | description, 46 | slug, 47 | rating = false, 48 | more = false, 49 | user, 50 | ...props 51 | }) => { 52 | const More = more ? ( 53 | 54 | {process.env.SHOW_SPEAKER && } 55 | {user && } 56 | 57 | 58 | ) : null; 59 | 60 | return ( 61 |
62 |

63 | 67 | {title} 68 | 69 |

70 |

{description}

71 | {More} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /hocs/withToken.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function setToken({ res, query }) { 4 | const cookie = require('cookie'); 5 | res.setHeader( 6 | 'Set-Cookie', 7 | cookie.serialize('token', String(query.token), { 8 | maxAge: 60 * 60 * 24 * 7, // 1 week 9 | }) 10 | ); 11 | } 12 | 13 | export const appWithToken = App => { 14 | return class AppWithToken extends React.Component { 15 | static async getInitialProps(appContext) { 16 | const { ctx } = appContext; 17 | const { query = {}, req } = ctx; 18 | 19 | if (req && query.token) { 20 | setToken(ctx); 21 | ctx.token = query.token; 22 | } 23 | 24 | let appProps = {}; 25 | if (typeof App.getInitialProps === 'function') { 26 | appProps = await App.getInitialProps.call(App, appContext); 27 | } 28 | 29 | return { 30 | ...appProps, 31 | }; 32 | } 33 | 34 | render() { 35 | return ; 36 | } 37 | }; 38 | }; 39 | 40 | export default Component => { 41 | return class extends React.Component { 42 | static async getInitialProps(ctx) { 43 | let props = {}; 44 | 45 | const { query = {}, req } = ctx; 46 | 47 | if (req && query.token) { 48 | setToken(ctx); 49 | ctx.token = query.token; 50 | } 51 | 52 | if (typeof Component.getInitialProps === 'function') { 53 | props = await Component.getInitialProps(ctx); 54 | } 55 | 56 | return { ...props }; 57 | } 58 | render() { 59 | return ; 60 | } 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /hocs/withUser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fetch from 'isomorphic-unfetch'; 3 | 4 | const API = process.env.API; 5 | 6 | function getToken({ req, token }) { 7 | if (token) { 8 | return token; 9 | } 10 | 11 | if (req) { 12 | const cookie = require('cookie'); 13 | 14 | if (!req.headers || !req.headers.cookie) { 15 | return; 16 | } 17 | 18 | const { token } = cookie.parse(req.headers.cookie); 19 | return token; 20 | } else { 21 | const cookie = require('js-cookie'); 22 | return cookie.get('token'); 23 | } 24 | } 25 | 26 | async function getUser(token) { 27 | const res = await fetch(`${API}/user`, { 28 | credentials: 'include', 29 | mode: 'cors', 30 | headers: { 31 | authorization: `bearer ${token}`, 32 | }, 33 | }); 34 | 35 | if (res.status === 200) { 36 | return res.json(); 37 | } 38 | 39 | // throw new Error(`failed: ${res.status}`); 40 | return null; 41 | } 42 | 43 | export const appWithUser = App => { 44 | return class AppWithUser extends React.Component { 45 | static async getInitialProps(appContext) { 46 | const token = getToken(appContext.ctx); 47 | const user = await getUser(token); 48 | 49 | appContext.ctx.user = user; 50 | 51 | let appProps = {}; 52 | if (typeof App.getInitialProps === 'function') { 53 | appProps = await App.getInitialProps.call(App, appContext); 54 | } 55 | 56 | return { 57 | ...appProps, 58 | }; 59 | } 60 | 61 | render() { 62 | return ; 63 | } 64 | }; 65 | }; 66 | 67 | export default Component => { 68 | return class extends React.Component { 69 | static async getInitialProps(ctx) { 70 | let props = {}; 71 | 72 | const token = getToken(ctx); 73 | const user = await getUser(token); 74 | 75 | if (typeof Component.getInitialProps === 'function') { 76 | props = await Component.getInitialProps(ctx); 77 | } 78 | 79 | return { ...props, user }; 80 | } 81 | render() { 82 | return ; 83 | } 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /mock-api/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "schedule": [ 3 | { 4 | "break": true, 5 | "title": "Registration", 6 | "duration": 40, 7 | "slug": "_reg" 8 | }, 9 | { 10 | "break": true, 11 | "slug": "_opening", 12 | "title": "Opening remarks", 13 | "duration": 10, 14 | "description": null, 15 | "speaker": null, 16 | "twitter": null, 17 | "bio": null 18 | }, 19 | { 20 | "slug": "rethinking", 21 | "title": "Rethinking the Web Platform", 22 | "duration": 40, 23 | "description": 24 | "Evolving the web to improve education, accessibility, performance, productivity, and design.", 25 | "speaker": "James Kyle", 26 | "twitter": "thejameskyle", 27 | "bio": 28 | "I dropped out of high school at 16, made email templates for a marketing internship, then started selling WordPress installs for $50" 29 | }, 30 | { 31 | "slug": "standards", 32 | "title": 33 | "If you're going out of San Francisco, be sure to wear Web Standards in your hair", 34 | "duration": 40, 35 | "description": 36 | "We do like our Holy Wars, don't we?: tables vs CSS? Responsive vs mdot? React vs Angular? CSS or CSS-in-JS? Let's look at the real issue: getting the free and open web to the other 4 billion people.", 37 | "speaker": "Bruce Lawson", 38 | "twitter": "brucel", 39 | "bio": 40 | "1981, my Physics teacher made up a UK101 kit in a tiny computer room which I could use instead of Physical Exercise. So I programmed games." 41 | }, 42 | { 43 | "break": true, 44 | "title": "Coffee break (30 mins)", 45 | "duration": 30, 46 | "slug": "break1" 47 | }, 48 | { 49 | "slug": "webcomponents", 50 | "title": "How the web sausage gets made", 51 | "duration": 40, 52 | "description": 53 | "They say there's two things you never want to see made: sausages and web standards. Sooooo I'm going to tell you about both! How do browsers work? What are web components even? Everyone's using them and maybe you should too! 🆒🐱🎉", 54 | "speaker": "Monica Dinculescu", 55 | "twitter": "notwaldorf", 56 | "bio": 57 | "I found out my mum always won at the QBasic Gorrilas game bc she changed the other player's code to randomly miss. Then she taught me how." 58 | }, 59 | { 60 | "slug": "data", 61 | "title": "Lessons learned sciencing the web", 62 | "duration": 40, 63 | "description": 64 | "Discover what slows down modern apps on mobile and how to fix this.", 65 | "speaker": "Addy Osmani", 66 | "twitter": "addyosmani", 67 | "bio": 68 | "At 16, wrote a web browser in C++ to understand how HTML, CSS and JavaScript worked…15 years later, still learning" 69 | }, 70 | { 71 | "break": true, 72 | "title": "Lunch break (90 mins)", 73 | "duration": 90, 74 | "slug": "_lunch" 75 | }, 76 | { 77 | "slug": "passwords", 78 | "title": "My Password Doesn't Work!", 79 | "duration": 40, 80 | "description": 81 | "Security is important, but it doesn't have to be complex. Let's dispel myths and assuage fear associated with those linchpin of our online lives – passwords – and build toward a more secure and more usable web.", 82 | "speaker": "Blaine Cook", 83 | "twitter": "blaine", 84 | "bio": 85 | "A glowing green screen, still fascinating despite its sole output: \"SYNTAX ERROR\"; Years later, the moment I used ATA and ATDT to talk to a friend." 86 | }, 87 | { 88 | "slug": "memory", 89 | "title": "Memory: Don't Forget to Take Out the Garbage", 90 | "duration": 40, 91 | "description": 92 | "JavaScript does a remarkable job of hiding memory management from us. What's going on behind the scenes?", 93 | "speaker": "Katie Fenn", 94 | "twitter": "katie_fenn", 95 | "bio": 96 | "In 2001 I started making post signatures for a Final Fantasy forum, and then started coding new features for the forum itself." 97 | }, 98 | { 99 | "break": true, 100 | "title": "Cake break (30 mins)", 101 | "duration": 30, 102 | "slug": "_break2" 103 | }, 104 | { 105 | "slug": "art", 106 | "title": "Abstract art in a time of minification", 107 | "duration": 40, 108 | "description": 109 | "aesthetic is a major component of any medium for art, including the web, but one thing that has been bothering me lately is: what happened to \"view source\"? are we destroying aesthetic for the sake of tooling and in spite of access to our industry????¿¿¿¿", 110 | "speaker": "Jenn Schiffer", 111 | "twitter": "jennschiffer", 112 | "bio": 113 | "my kid laptop had a hangman game written in BASIC and i accessed the source and rigged it to cheat against my half sister!" 114 | }, 115 | { 116 | "slug": "comedy", 117 | "title": "Alpha, Beta, Gamer: Dev Mode", 118 | "duration": 40, 119 | "description": 120 | "A live performance of video games and stand up comedy from comedian and coder, including pre prepared web games to play and even creating a video game with the audience on stage in only 10 minutes.", 121 | "speaker": "Joe Hart", 122 | "twitter": "joehart", 123 | "bio": 124 | "Parents got me Lego Mindstorms for Xmas, used it and got frustrated that the software didn't let me do exactly what I wanted. Thus here I am" 125 | }, 126 | { 127 | "break": true, 128 | "title": "Closing remarks", 129 | "duration": 20, 130 | "slug": "_closing" 131 | }, 132 | { 133 | "slug": "_party", 134 | "break": true, 135 | "title": "After Party", 136 | "duration": 90, 137 | "description": null, 138 | "speaker": null, 139 | "twitter": null, 140 | "bio": null 141 | } 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /mock-api/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/schedule/:slug": "/schedule?slug=:slug" 3 | } 4 | -------------------------------------------------------------------------------- /mock-api/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 13700, 5 | "username": "remy", 6 | "avatar": "https://avatars0.githubusercontent.com/u/13700?v=4", 7 | "email": "remy@remysharp.com", 8 | "name": "Remy Sharp", 9 | "apikey": "6976562d-fb65-4f53-866e-7ddea74cb146" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | require('@remy/envy'); 2 | const webpack = require('webpack'); 3 | const withCSS = require('@zeit/next-css'); 4 | 5 | const withBundleAnalyzer = require('@zeit/next-bundle-analyzer'); 6 | 7 | module.exports = withCSS( 8 | withBundleAnalyzer({ 9 | analyzeServer: ['server', 'both'].includes(process.env.BUNDLE_ANALYZE), 10 | analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE), 11 | bundleAnalyzerConfig: { 12 | server: { 13 | analyzerMode: 'static', 14 | reportFilename: '../../bundles/server.html', 15 | }, 16 | browser: { 17 | analyzerMode: 'static', 18 | reportFilename: '../bundles/client.html', 19 | }, 20 | openAnalyzer: true, 21 | }, 22 | cssModules: false, 23 | webpack: config => { 24 | config.plugins.push( 25 | new webpack.EnvironmentPlugin([ 26 | 'SHOW_SPEAKER', 27 | 'PORT', 28 | 'API', 29 | // 'NOW_URL', 30 | ]) 31 | ); 32 | 33 | return config; 34 | }, 35 | ___exportPathMap: async function() { 36 | return { 37 | '/': { page: '/' }, 38 | '/about': { page: '/about' }, 39 | '/contact': { page: '/contact' }, 40 | '/session/memory': { page: '/session', query: { slug: 'memory' } }, 41 | '/session/rethinking': { 42 | page: '/session', 43 | query: { slug: 'rethinking' }, 44 | }, 45 | '/session/passwords': { 46 | page: '/session', 47 | query: { slug: 'passwords' }, 48 | }, 49 | '/session/art': { page: '/session', query: { slug: 'art' } }, 50 | }; 51 | }, 52 | }) 53 | ); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next.training", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "now": { 7 | "alias": "next-workshop" 8 | }, 9 | "scripts": { 10 | "dev": "node server.js", 11 | "build": "NODE_ENV=production next build && next export", 12 | "start": "NODE_ENV=production node server.js", 13 | "mock-api": "json-server mock-api/db.json --port 3001 --routes mock-api/routes.json", 14 | "dev-server": "PORT=3001 envy -- router ./routes/index.js" 15 | }, 16 | "engines": { 17 | "node": ">=8.3.x" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@zeit/next-bundle-analyzer": "^0.1.1", 24 | "@zeit/next-css": "^0.1.5", 25 | "@zeit/next-sass": "^0.1.2", 26 | "body-parser": "^1.18.2", 27 | "cookie": "^0.3.1", 28 | "cookie-parser": "^1.4.3", 29 | "cors": "^2.8.4", 30 | "express": "^4.16.3", 31 | "express-session": "^1.15.6", 32 | "isomorphic-unfetch": "^2.0.0", 33 | "js-cookie": "^2.2.0", 34 | "jsonwebtoken": "^8.2.0", 35 | "lowdb": "^1.0.0", 36 | "ms": "^2.1.1", 37 | "next": "^6.0.3", 38 | "nick-generator": "^1.0.0", 39 | "node-sass": "^4.8.3", 40 | "nprogress": "^0.2.0", 41 | "passport": "^0.4.0", 42 | "passport-github2": "^0.1.11", 43 | "react": "^16.2.0", 44 | "react-codemirror": "^1.0.0", 45 | "react-dom": "^16.2.0", 46 | "request": "^2.85.0", 47 | "session-file-store": "^1.2.0", 48 | "slug": "^0.9.1", 49 | "undefsafe": "^2.0.2", 50 | "uuid": "^3.2.1" 51 | }, 52 | "devDependencies": { 53 | "@remy/envy": "^3.1.1", 54 | "babel-eslint": "^8.2.2", 55 | "eslint": "^4.19.1", 56 | "eslint-plugin-node": "^6.0.1", 57 | "eslint-plugin-react": "^7.7.0", 58 | "express-router-cli": "^1.5.2", 59 | "json-server": "^0.12.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, { Container } from 'next/app'; 2 | import Layout from '../components/Layout'; 3 | import { appWithUser } from '../hocs/withUser'; 4 | import { appWithToken } from '../hocs/withToken'; 5 | 6 | class MyApp extends App { 7 | static async getInitialProps({ Component, router, ctx }) { 8 | let pageProps = {}; 9 | 10 | if (Component.getInitialProps) { 11 | pageProps = await Component.getInitialProps(ctx); 12 | } 13 | 14 | return { pageProps: { ...pageProps, user: ctx.user } }; 15 | } 16 | 17 | render() { 18 | const { Component, pageProps } = this.props; 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default appWithToken(appWithUser(MyApp)); 30 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document'; 2 | 3 | export default class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 |