├── .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 |
23 |
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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pages/_error.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export default class Error extends Component {
4 | static getInitialProps({ res, err }) {
5 | const statusCode = res ? res.statusCode : err ? err.statusCode : null;
6 | return { statusCode, err: err ? err.message : '' };
7 | }
8 |
9 | render() {
10 | return (
11 |
12 | {this.props.err}:
13 | {this.props.statusCode
14 | ? `An error ${this.props.statusCode} occurred on server`
15 | : 'An error occurred on client'}
16 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pages/about.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | // title="About the app"
4 | export default () => (
5 | <>
6 | About
7 | This site will a conference schedule…eventually!
8 |
9 |
10 | Get in touch
11 |
12 |
13 | >
14 | );
15 |
--------------------------------------------------------------------------------
/pages/auth/callback.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'next/router';
3 | import { NextAuth } from 'next-auth/client';
4 |
5 | export default class extends React.Component {
6 | static async getInitialProps({ req }) {
7 | return {
8 | session: await NextAuth.init({ force: true, req: req }),
9 | };
10 | }
11 |
12 | async componentDidMount() {
13 | // Get latest session data after rendering on client then redirect.
14 | // The ensures client state is always updated after signing in or out.
15 | await NextAuth.init({ force: true });
16 | Router.push('/');
17 | }
18 |
19 | render() {
20 | return Loading…
;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/pages/auth/check-email.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remy/next-training/75883b066c73de31c201534f42b6e65a6340cc71/pages/auth/check-email.js
--------------------------------------------------------------------------------
/pages/auth/error.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remy/next-training/75883b066c73de31c201534f42b6e65a6340cc71/pages/auth/error.js
--------------------------------------------------------------------------------
/pages/auth/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remy/next-training/75883b066c73de31c201534f42b6e65a6340cc71/pages/auth/index.js
--------------------------------------------------------------------------------
/pages/contact.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 | <>
3 | Contact
4 | There's lots of ways to contact me:
5 |
16 | >
17 | );
18 |
--------------------------------------------------------------------------------
/pages/explode.js:
--------------------------------------------------------------------------------
1 | function explode() {
2 | throw new Error('explode!');
3 | }
4 |
5 | export default () => Explode! {explode()}
;
6 |
--------------------------------------------------------------------------------
/pages/index-class.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import fetch from 'isomorphic-unfetch';
3 | import Session from '../components/Session';
4 | import Layout from '../components/Layout';
5 |
6 | class Index extends Component {
7 | static async getInitialProps() {
8 | const res = await fetch('http://localhost:3001/schedule');
9 | const schedule = await res.json();
10 |
11 | return { schedule };
12 | }
13 |
14 | render() {
15 | const { schedule } = this.props;
16 | return (
17 |
18 | NextConf Schedule Browser
19 | {schedule.map(s => )}
20 |
21 | );
22 | }
23 | }
24 |
25 | export default Index;
26 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 | import Session from '../components/Session';
3 | import withToken from '../hocs/withToken';
4 |
5 | const API = process.env.API || process.env.NOW_URL;
6 |
7 | const Index = ({ schedule = [], ...props }) => (
8 | <>
9 | NextConf Schedule Browser
10 | {schedule.map(s => )}
11 | >
12 | );
13 |
14 | Index.getInitialProps = async () => {
15 | const res = await fetch(`${API}/schedule`);
16 | const schedule = await res.json();
17 |
18 | return { schedule };
19 | };
20 |
21 | export default Index;
22 |
--------------------------------------------------------------------------------
/pages/session.js:
--------------------------------------------------------------------------------
1 | import Layout from '../components/Layout';
2 | import Session from '../components/Session';
3 | import fetch from 'isomorphic-unfetch';
4 |
5 | const API = process.env.API || process.env.NOW_URL;
6 |
7 | const SessionPage = ({ session, rating, ...props }) => (
8 | <>
9 |
10 | >
11 | );
12 |
13 | SessionPage.getInitialProps = async ({ query, user }) => {
14 | console.log('SessionPage.getInitialProps', user);
15 | const res = await fetch(`${API}/schedule/${query.slug}`, {
16 | mode: 'cors',
17 | });
18 | const session = await res.json();
19 |
20 | return { session, rating: query.rating };
21 | };
22 |
23 | export default SessionPage;
24 |
--------------------------------------------------------------------------------
/routes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['node'],
3 | extends: ['eslint:recommended', 'plugin:node/recommended'],
4 | parser: 'babel-eslint',
5 | env: {
6 | node: true,
7 | },
8 | rules: {
9 | 'node/exports-style': ['error', 'module.exports'],
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport');
2 | const express = require('express');
3 | const request = require('request');
4 |
5 | // user auth
6 | const Strategy = require('passport-github2').Strategy;
7 | const cookieParser = require('cookie-parser');
8 | const expressSession = require('express-session');
9 | const SessionStore = require('session-file-store')(expressSession);
10 |
11 | const tmpdir = require('os').tmpdir();
12 | const User = require('./db/user');
13 | const router = express.Router();
14 |
15 | module.exports = router;
16 |
17 | passport.serializeUser((user, done) => {
18 | done(null, user.id);
19 | });
20 |
21 | passport.deserializeUser((id, done) => {
22 | User.findOne({ id })
23 | .then(user => {
24 | done(null, user);
25 | })
26 | .catch(done);
27 | });
28 |
29 | passport.use(
30 | 'github',
31 | new Strategy(
32 | {
33 | clientID: process.env.GITHUB_CLIENT_ID,
34 | clientSecret: process.env.GITHUB_SECRET,
35 | // callbackURL: process.env.GITHUB_CALLBACK,
36 | },
37 | (accessToken, refreshToken, profile, done) => {
38 | const {
39 | login: username,
40 | id,
41 | avatar_url: avatar,
42 | email,
43 | name,
44 | } = profile._json;
45 | User.findOrCreate({ id }, { username, id, avatar, email, name })
46 | .then(user => {
47 | if (user.email) {
48 | return done(null, user);
49 | }
50 |
51 | // otherwise go get their email address and store it
52 | request(
53 | {
54 | url: 'https://api.github.com/user/emails',
55 | json: true,
56 | headers: {
57 | 'user-agent':
58 | 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)',
59 | authorization: `token ${accessToken}`,
60 | },
61 | },
62 | (error, res, body) => {
63 | if (error) {
64 | return done(null, user);
65 | }
66 |
67 | user.email = body.find(_ => _.primary).email;
68 |
69 | user
70 | .save()
71 | .then(user => done(null, user))
72 | .catch(done);
73 | }
74 | );
75 | })
76 | .catch(e => done(e));
77 | }
78 | )
79 | );
80 |
81 | router.use(cookieParser());
82 | router.use(
83 | expressSession({
84 | resave: true,
85 | secret: process.env.SESSION_SECRET || Math.random(),
86 | name: 'id',
87 | httpOnly: true,
88 | saveUninitialized: true,
89 | cookie: {
90 | maxAge: 60 * 60 * 24 * 60 * 1000, // milliseconds
91 | },
92 | store: new SessionStore({ path: tmpdir + '/sessions' }),
93 | })
94 | );
95 | router.use(passport.initialize());
96 | router.use(passport.session());
97 |
98 | router.get('/auth', passport.authenticate('github', { scope: ['user:email'] }));
99 |
100 | router.get(
101 | '/auth/callback',
102 | passport.authenticate('github', {
103 | failureRedirect: '/login',
104 | // session: false,
105 | }),
106 | (req, res) => {
107 | res.redirect(`${process.env.HOST}/?token=${req.user.bearer()}`);
108 | }
109 | );
110 |
--------------------------------------------------------------------------------
/routes/cors.js:
--------------------------------------------------------------------------------
1 | const cors = require('cors');
2 | module.exports = cors({
3 | origin: true,
4 | credentials: true,
5 | allowedHeaders: [
6 | 'Authorization',
7 | 'Origin',
8 | 'X-Requested-With',
9 | 'Content-Type',
10 | 'Accept',
11 | ],
12 | });
13 |
--------------------------------------------------------------------------------
/routes/db/index.js:
--------------------------------------------------------------------------------
1 | const low = require('lowdb');
2 | const FileSync = require('lowdb/adapters/FileSync');
3 |
4 | const adapter = new FileSync(__dirname + '/../../mock-api/users.json');
5 | const db = low(adapter);
6 |
7 | // Set some defaults (required if your JSON file is empty)
8 | db.defaults({ users: [] }).write();
9 |
10 | module.exports = db;
11 |
--------------------------------------------------------------------------------
/routes/db/user.js:
--------------------------------------------------------------------------------
1 | const db = require('./index');
2 | const uuid = require('uuid').v4;
3 | const jwt = require('jsonwebtoken');
4 |
5 | const unique = 'id';
6 |
7 | class User {
8 | constructor(props) {
9 | Object.assign(this, props);
10 | this._store = null;
11 | }
12 |
13 | static findOne(key) {
14 | const user = db.get('users').find(key);
15 |
16 | if (!user.value()) {
17 | return Promise.resolve(null);
18 | }
19 |
20 | const u = new User(user.value());
21 | u._store = user;
22 |
23 | return Promise.resolve(u);
24 | }
25 |
26 | static async findOrCreate(key, values) {
27 | const user = await User.findOne(key);
28 |
29 | if (!user) {
30 | return new User(values).save();
31 | }
32 |
33 | return user;
34 | }
35 |
36 | bearer(expiresIn = '1 hour') {
37 | const { username, id, avatar } = this;
38 | return jwt.sign({ username, id, avatar }, this.apikey, { expiresIn });
39 | }
40 |
41 | toObject() {
42 | return this._store.value();
43 | }
44 |
45 | update(values) {
46 | Object.assign(this, values);
47 | return this;
48 | }
49 |
50 | async save() {
51 | const props = Object.getOwnPropertyNames(this).reduce((acc, curr) => {
52 | if (!curr.startsWith('_')) {
53 | acc[curr] = this[curr];
54 | }
55 | return acc;
56 | }, {});
57 |
58 | if (this._store) {
59 | this._store = this._store.assign(props);
60 | this._store.write();
61 | } else {
62 | if (await User.findOne({ [unique]: props[unique] })) {
63 | return Promise.reject(new Error('DUPLICATE_ENTRY'));
64 | }
65 |
66 | const data = { id: uuid(), ...props, apikey: uuid() };
67 | db
68 | .get('users')
69 | .push(data)
70 | .write();
71 |
72 | this._store = db.get('users').find(data);
73 |
74 | this.update(this._store.value());
75 | }
76 |
77 | return Promise.resolve(this);
78 | }
79 | }
80 |
81 | module.exports = User;
82 |
--------------------------------------------------------------------------------
/routes/db/user.test.js:
--------------------------------------------------------------------------------
1 | console.log('\033c'); // cls
2 | const User = require('./User');
3 | const db = require('./index');
4 |
5 | db
6 | .get('users')
7 | .remove()
8 | .write();
9 |
10 | async function __main() {
11 | const u = {
12 | id: 123,
13 | login: 'rem',
14 | };
15 |
16 | await User.findOrCreate({ id: u.id }, u);
17 |
18 | const d = db.get('users');
19 |
20 | let s = d.push(u).write();
21 |
22 | s = d.find({ id: u.id });
23 | s.assign({ name: 'remy' }).write();
24 |
25 | console.log(db.value());
26 |
27 | // store.assign({ other: 'bit ' });
28 | // store.write();
29 |
30 | console.log(db.get('users').value().length);
31 | }
32 |
33 | async function main() {
34 | const user = await User.findOrCreate(
35 | {
36 | id: 123,
37 | },
38 | {
39 | id: 123,
40 | login: 'rem',
41 | location: 'brighton',
42 | newbit: 'ok',
43 | }
44 | );
45 |
46 | user.updated = new Date().toJSON();
47 |
48 | await user.save();
49 |
50 | console.log(db.get('users').value());
51 | }
52 |
53 | try {
54 | main();
55 | } catch (e) {
56 | console.log(e);
57 | }
58 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const auth = require('./auth');
4 | const data = require('../mock-api/db.json');
5 | const cors = require('./cors');
6 |
7 | module.exports = router;
8 |
9 | router.options('/*', (req, res) => {
10 | res.status(204).send('');
11 | });
12 |
13 | router.use(cors);
14 |
15 | router.use(auth);
16 |
17 | router.use('/user', require('./user'));
18 | router.get('/schedule', (req, res) => {
19 | res.json(data.schedule);
20 | });
21 |
22 | router.get('/schedule/:slug', (req, res) => {
23 | res.json(data.schedule.find(s => s.slug === req.params.slug));
24 | });
25 |
--------------------------------------------------------------------------------
/routes/strategy/github.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport');
2 | const request = require('request');
3 | const User = require('../db/user');
4 | const Strategy = require('passport-github2').Strategy;
5 |
6 | const strategy = new Strategy(
7 | {
8 | clientID: process.env.GITHUB_CLIENT_ID,
9 | clientSecret: process.env.GITHUB_SECRET,
10 | // callbackURL: process.env.GITHUB_CALLBACK,
11 | },
12 | (accessToken, refreshToken, profile, done) => {
13 | User.findOrCreate({ githubId: profile.id }, profile._json)
14 | .then(user => {
15 | if (user.email) {
16 | return done(null, user);
17 | }
18 |
19 | // otherwise go get their email address and store it
20 | request(
21 | {
22 | url: 'https://api.github.com/user/emails',
23 | json: true,
24 | headers: {
25 | 'user-agent':
26 | 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)',
27 | authorization: `token ${accessToken}`,
28 | },
29 | },
30 | (error, res, body) => {
31 | if (error) {
32 | return done(null, user);
33 | }
34 |
35 | user.email = body.find(_ => _.primary).email;
36 |
37 | user
38 | .save()
39 | .then(user => done(null, user))
40 | .catch(done);
41 | }
42 | );
43 | })
44 | .catch(e => done(e));
45 | }
46 | );
47 |
48 | module.exports = {
49 | strategy,
50 | root: passport.authenticate('github', { scope: ['user:email'] }),
51 | callback: passport.authenticate('github', { failureRedirect: '/login' }),
52 | };
53 |
--------------------------------------------------------------------------------
/routes/user.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const User = require('./db/user');
3 | const { json } = require('body-parser');
4 | const nickGenerator = require('nick-generator');
5 | const slug = require('slug');
6 | const jwt = require('jsonwebtoken');
7 | const router = express.Router();
8 |
9 | module.exports = router;
10 | router.use(json());
11 |
12 | function makeUsername(username = nickGenerator()) {
13 | return slug(username).toLowerCase();
14 | }
15 |
16 | function authRequired(req, res, next) {
17 | if (!req.user) {
18 | return res.status(401).json(401); // unauthorized
19 | }
20 |
21 | next();
22 | }
23 |
24 | function find(username) {
25 | return User.findOne({ username });
26 | }
27 |
28 | router.use(async (req, res, next) => {
29 | const [type, token] = (req.headers['authorization'] || '').split(' ', 2);
30 |
31 | if (token) {
32 | if (type === 'token') {
33 | const user = await User.findOne({ apikey: token });
34 | if (user) {
35 | req.user = user;
36 | } else {
37 | req.user = null;
38 | }
39 | return next();
40 | }
41 |
42 | // bearer
43 | // 1. decode
44 | // 2. lookup
45 | // 3. validate
46 |
47 | const decoded = jwt.decode(token);
48 |
49 | if (!decoded) {
50 | return next();
51 | }
52 |
53 | const user = await User.findOne({ id: decoded.id });
54 |
55 | if (!user) {
56 | req.user = null;
57 | return next();
58 | }
59 |
60 | try {
61 | jwt.verify(token, user.apikey); // if jwt has expired, it will throw
62 | req.user = user;
63 | } catch (e) {
64 | return res.status(401).json(401);
65 | }
66 | }
67 |
68 | next();
69 | });
70 |
71 | router.get('/', authRequired, (req, res) => res.json(req.user.toObject()));
72 |
73 | router.put('/', authRequired, async (req, res) => {
74 | await req.user.update(req.body).save();
75 | res.status(200).json(true);
76 | });
77 |
78 | router.post('/', async (req, res) => {
79 | const username = makeUsername(req.body.username);
80 | if (await find(username)) {
81 | return res.status(406).json(406); // not accepted
82 | }
83 |
84 | const user = await new User({
85 | password: 'let me in',
86 | ...req.body,
87 | username,
88 | token: Date.now().toString(),
89 | }).save();
90 | res.status(201).json(user.toObject());
91 | });
92 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('@remy/envy');
2 | const express = require('express');
3 | const next = require('next');
4 | const dev = process.env.NODE_ENV !== 'production';
5 | const app = next({ dev });
6 | const handle = app.getRequestHandler();
7 |
8 | app
9 | .prepare()
10 | .then(() => {
11 | const server = express();
12 |
13 | // custom handlers go here…
14 | server.use(require('./routes'));
15 |
16 | server.get('/session/:slug', (req, res) => {
17 | const { slug } = req.params;
18 | app.render(req, res, '/session', { ...req.query, slug });
19 | });
20 |
21 | server.get('*', (req, res) => handle(req, res));
22 |
23 | server.listen(process.env.PORT, err => {
24 | if (err) throw err;
25 | console.log('> Ready on http://localhost:3000');
26 | });
27 | })
28 | .catch(ex => {
29 | console.error(ex.stack);
30 | process.exit(1);
31 | });
32 |
--------------------------------------------------------------------------------
/static/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: hotpink;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px hotpink, 0 0 5px hotpink;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: hotpink;
49 | border-left-color: hotpink;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/static/remy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remy/next-training/75883b066c73de31c201534f42b6e65a6340cc71/static/remy.jpg
--------------------------------------------------------------------------------
/static/styles.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | html,
7 | body {
8 | margin: 0;
9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
10 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
11 | sans-serif;
12 | color: #424242;
13 | font-size: 14px;
14 | line-height: 1.4;
15 | }
16 |
17 | #root {
18 | display: flex;
19 | flex-direction: column;
20 | min-height: 100vh;
21 | }
22 |
23 | select {
24 | font-size: 1rem;
25 | }
26 |
27 | a {
28 | color: rgb(33, 150, 243);
29 | text-decoration: none;
30 | }
31 |
32 | a:hover {
33 | text-decoration: underline;
34 | }
35 |
36 | main {
37 | flex-grow: 1;
38 | }
39 |
40 | #root > header {
41 | background: rgb(33, 150, 243);
42 | }
43 |
44 | #root > main,
45 | #root > footer,
46 | #root > header > nav {
47 | margin: 0 auto;
48 | width: 100%;
49 | max-width: 600px;
50 | padding: 10px;
51 | }
52 |
53 | header nav > * {
54 | padding: 10px;
55 | display: inline-block;
56 | margin-right: 10px;
57 | color: white;
58 | /* border: 2px solid transparent; */
59 | }
60 |
61 | header nav a:hover {
62 | background: white;
63 | color: rgb(33, 150, 243);
64 | text-decoration: none;
65 | }
66 |
67 | /* for another day :) */
68 | header nav a.active {
69 | border: 2px solid rgb(138, 199, 245);
70 | }
71 |
72 | blockquote {
73 | font: italic 300 system-ui;
74 | }
75 |
76 | p {
77 | font: 400 system-ui;
78 | }
79 |
80 | h1,
81 | h2,
82 | h3 {
83 | font-weight: 500;
84 | color: #212121;
85 | }
86 |
87 | .Rating {
88 | float: left;
89 | }
90 |
91 | /* :not(:checked) is a filter, so that browsers that don’t support :checked don’t
92 | follow these rules. Every browser that supports :checked also supports :not(), so
93 | it doesn’t make the test unnecessarily selective */
94 | .Rating:not(:checked) > input {
95 | position: absolute;
96 | top: -9999px;
97 | clip: rect(0, 0, 0, 0);
98 | }
99 |
100 | .Rating:not(:checked) > label {
101 | float: right;
102 | width: 1.1em;
103 | padding: 0 0.1em;
104 | overflow: hidden;
105 | white-space: nowrap;
106 | cursor: pointer;
107 | font-size: 200%;
108 | line-height: 1.2;
109 | color: #ddd;
110 | /* text-shadow: 1px 1px #bbb; */
111 | /* , 2px 2px #666, 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.5); */
112 | }
113 |
114 | .Rating:not(:checked) > label:before {
115 | content: '★ ';
116 | }
117 |
118 | .Rating > input:checked ~ label {
119 | color: #f70;
120 | text-shadow: 1px 1px #c60;
121 | /* 2px 2px #940, 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.5); */
122 | }
123 |
124 | .Rating:not(:checked) > label:hover,
125 | .Rating:not(:checked) > label:hover ~ label {
126 | color: gold;
127 | text-shadow: 1px 1px goldenrod;
128 | /* 2px 2px #b57340,
129 | 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.5); */
130 | }
131 |
132 | .Rating > input:checked + label:hover,
133 | .Rating > input:checked + label:hover ~ label,
134 | .Rating > input:checked ~ label:hover,
135 | .Rating > input:checked ~ label:hover ~ label,
136 | .Rating > label:hover ~ input:checked ~ label {
137 | color: #ea0;
138 | text-shadow: 1px 1px goldenrod;
139 | /* 2px 2px #b57340,
140 | 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.5); */
141 | }
142 |
143 | .Rating > label:active {
144 | position: relative;
145 | top: 2px;
146 | left: 2px;
147 | }
148 |
--------------------------------------------------------------------------------