diff --git a/book/9-begin/.ebextensions/environment.config b/book/9-begin/.ebextensions/environment.config new file mode 100644 index 00000000..cd9da5f0 --- /dev/null +++ b/book/9-begin/.ebextensions/environment.config @@ -0,0 +1,3 @@ +option_settings: + - option_name: NODE_ENV + value: production \ No newline at end of file diff --git a/book/9-begin/.ebextensions/git.config b/book/9-begin/.ebextensions/git.config new file mode 100644 index 00000000..1efdaaab --- /dev/null +++ b/book/9-begin/.ebextensions/git.config @@ -0,0 +1,3 @@ +packages: + yum: + git: [] \ No newline at end of file diff --git a/book/9-begin/.elasticbeanstalk/config.yml b/book/9-begin/.elasticbeanstalk/config.yml new file mode 100644 index 00000000..b25e3b28 --- /dev/null +++ b/book/9-begin/.elasticbeanstalk/config.yml @@ -0,0 +1,22 @@ +branch-defaults: + default: + environment: builderbook-8-end +environment-defaults: + async-github1-env: + branch: null + repository: null + builderbook-app: + branch: null + repository: null +global: + application_name: book + default_ec2_keyname: null + default_platform: Node.js + default_region: us-east-1 + include_git_submodules: true + instance_profile: null + platform_name: null + platform_version: null + profile: null + sc: null + workspace_type: Application diff --git a/book/9-begin/.eslintrc.js b/book/9-begin/.eslintrc.js new file mode 100644 index 00000000..3bd91ccd --- /dev/null +++ b/book/9-begin/.eslintrc.js @@ -0,0 +1,55 @@ +module.exports = { + parser: 'babel-eslint', + extends: ['airbnb', 'plugin:prettier/recommended'], + env: { + browser: true, + jest: true, + }, + plugins: ['react', 'jsx-a11y', 'import', 'prettier'], + rules: { + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'all', + arrowParens: 'always', + printWidth: 100, + semi: true + }, + ], + 'camelcase': 'off', + 'max-len': ['error', 100], + 'no-underscore-dangle': ['error', { allow: ['_id'] }], + 'no-mixed-operators': 'off', + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true, + }, + AssignmentExpression: { + array: true, + object: false, + }, + }, + { + enforceForRenamedProperties: false, + }, + ], + 'import/prefer-default-export': 'off', + 'jsx-a11y/anchor-is-valid': 'off', + 'react/jsx-wrap-multilines': 'off', + 'react/destructuring-assignment': 'off', + 'react/no-danger': 'off', + 'react/jsx-one-expression-per-line': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/jsx-filename-extension': [ + 'error', + { + extensions: ['.js'], + }, + ], + }, +}; diff --git a/book/9-begin/.gitignore b/book/9-begin/.gitignore new file mode 100755 index 00000000..5f8e6d8a --- /dev/null +++ b/book/9-begin/.gitignore @@ -0,0 +1,14 @@ +*~ +*.swp +tmp/ +npm-debug.log +.DS_Store + + +.build/* +.next +.vscode/ +node_modules/ +.coverage +.env +.next \ No newline at end of file diff --git a/book/9-begin/components/Header.js b/book/9-begin/components/Header.js new file mode 100644 index 00000000..1404c572 --- /dev/null +++ b/book/9-begin/components/Header.js @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import Link from 'next/link'; +import Toolbar from '@material-ui/core/Toolbar'; +import Grid from '@material-ui/core/Grid'; +import Hidden from '@material-ui/core/Hidden'; +import Button from '@material-ui/core/Button'; +import Avatar from '@material-ui/core/Avatar'; + +import MenuDrop from './MenuDrop'; + +import { styleToolbar, styleRaisedButton } from './SharedStyles'; + +const optionsMenuCustomer = [ + { + text: 'My books', + href: '/customer/my-books', + as: '/my-books', + }, + { + text: 'Log out', + href: '/logout', + }, +]; + +const optionsMenuAdmin = [ + { + text: 'Admin', + href: '/admin', + }, + { + text: 'Log out', + href: '/logout', + }, +]; + + + +function Header({ user, hideHeader, redirectUrl }) { + return ( +
+ + + + {!user ? ( + + + + ) : null} + + + {user && user.isAdmin && !user.isGithubConnected ? ( + + + + + + ) : null} + + + {user ? ( +
+ {!user.isAdmin ? ( + + ) : null} + {user.isAdmin ? ( + + ) : null} +
+ ) : ( + + Log in + + )} +
+
+
+
+ ); +} + +const propTypes = { + user: PropTypes.shape({ + avatarUrl: PropTypes.string, + displayName: PropTypes.string, + isAdmin: PropTypes.bool, + isGithubConnected: PropTypes.bool, + }), + hideHeader: PropTypes.bool, + redirectUrl: PropTypes.string, +}; + +const defaultProps = { + user: null, + hideHeader: false, + redirectUrl: '', +}; + +Header.propTypes = propTypes; +Header.defaultProps = defaultProps; + +export default Header; diff --git a/book/9-begin/components/MenuDrop.js b/book/9-begin/components/MenuDrop.js new file mode 100644 index 00000000..f8dbf59e --- /dev/null +++ b/book/9-begin/components/MenuDrop.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Link from 'next/link'; +import Menu from '@material-ui/core/Menu'; +import Avatar from '@material-ui/core/Avatar'; + +const propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + options: PropTypes.arrayOf(String).isRequired, +}; + +class MenuDrop extends React.Component { + // eslint-disable-next-line + state = { + open: false, + anchorEl: undefined, + }; + + button = undefined; + + handleClick = (event) => { + this.setState({ open: true, anchorEl: event.currentTarget }); + }; + + handleClose = () => { + this.setState({ open: false }); + }; + + render() { + const { options, src, alt } = this.props; + + return ( +
+ + +

+ {options.map((option) => ( +

+ ))} +
+
+ ); + } +} + +MenuDrop.propTypes = propTypes; + +export default MenuDrop; diff --git a/book/9-begin/components/Notifier.js b/book/9-begin/components/Notifier.js new file mode 100644 index 00000000..4aa53f99 --- /dev/null +++ b/book/9-begin/components/Notifier.js @@ -0,0 +1,52 @@ +import React from 'react'; +import Snackbar from '@material-ui/core/Snackbar'; + +let openSnackbarFn; + +class Notifier extends React.Component { + // eslint-disable-next-line + state = { + open: false, + message: '', + }; + + componentDidMount() { + openSnackbarFn = this.openSnackbar; + } + + handleSnackbarRequestClose = () => { + this.setState({ + open: false, + message: '', + }); + }; + + openSnackbar = ({ message }) => { + this.setState({ open: true, message }); + }; + + render() { + const message = ( + + ); + + return ( + + ); + } +} + +export function openSnackbar({ message }) { + openSnackbarFn({ message }); +} + +export default Notifier; diff --git a/book/9-begin/components/SharedStyles.js b/book/9-begin/components/SharedStyles.js new file mode 100644 index 00000000..bcc3b345 --- /dev/null +++ b/book/9-begin/components/SharedStyles.js @@ -0,0 +1,54 @@ +const styleBigAvatar = { + width: '80px', + height: '80px', + margin: '0px auto 15px', +}; + +const styleRaisedButton = { + font: '16px', +}; + +const styleToolbar = { + background: '#FFF', + height: '64px', + paddingRight: '20px', +}; + +const styleLoginButton = { + borderRadius: '2px', + textTransform: 'none', + font: '16px', + fontWeight: '400', + letterSpacing: '0.01em', + color: 'white', + backgroundColor: '#DF4930', +}; + +const styleTextField = { + font: '15px', + color: '#222', + fontWeight: '300', +}; + +const styleForm = { + margin: '7% auto', + width: '360px', +}; + +const styleGrid = { + margin: '0px auto', + font: '16px', + color: '#222', + fontWeight: '300', + lineHeight: '1.5em', +}; + +module.exports = { + styleBigAvatar, + styleRaisedButton, + styleToolbar, + styleLoginButton, + styleTextField, + styleForm, + styleGrid, +}; diff --git a/book/9-begin/components/admin/EditBook.js b/book/9-begin/components/admin/EditBook.js new file mode 100644 index 00000000..05ec05cd --- /dev/null +++ b/book/9-begin/components/admin/EditBook.js @@ -0,0 +1,137 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Input from '@material-ui/core/Input'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; + +import { getGithubRepos } from '../../lib/api/admin'; +import { styleTextField } from '../SharedStyles'; +import notify from '../../lib/notifier'; + +const propTypes = { + book: PropTypes.shape({ + _id: PropTypes.string.isRequired, + }), + onSave: PropTypes.func.isRequired, +}; + +const defaultProps = { + book: null, +}; + +class EditBook extends React.Component { + constructor(props) { + super(props); + + this.state = { + book: props.book || {}, + repos: [], + }; + } + + async componentDidMount() { + try { + const { repos } = await getGithubRepos(); + this.setState({ repos }); // eslint-disable-line + } catch (err) { + console.log(err); // eslint-disable-line + } + } + + onSubmit = (event) => { + event.preventDefault(); + const { name, price, githubRepo } = this.state.book; + + if (!name) { + notify('Name is required'); + return; + } + + if (!price) { + notify('Price is required'); + return; + } + + if (!githubRepo) { + notify('Github repo is required'); + return; + } + + this.props.onSave(this.state.book); + }; + + render() { + return ( +
+
+
+
+ { + this.setState({ + book: { ...this.state.book, name: event.target.value }, + }); + }} + value={this.state.book.name} + type="text" + label="Book's title" + style={styleTextField} + required + /> +
+
+
+ { + this.setState({ + book: { ...this.state.book, price: Number(event.target.value) }, + }); + }} + value={this.state.book.price} + type="number" + label="Book's price" + className="textFieldInput" + style={styleTextField} + step="1" + required + /> +
+
+
+ Github repo: + +
+
+
+ + +
+ ); + } +} + +EditBook.propTypes = propTypes; +EditBook.defaultProps = defaultProps; + +export default EditBook; diff --git a/book/9-begin/components/customer/BuyButton.js b/book/9-begin/components/customer/BuyButton.js new file mode 100644 index 00000000..86cb7fb1 --- /dev/null +++ b/book/9-begin/components/customer/BuyButton.js @@ -0,0 +1,130 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import NProgress from 'nprogress'; +import Button from '@material-ui/core/Button'; +// import Link from 'next/link'; +import { loadStripe } from '@stripe/stripe-js'; + +import { fetchCheckoutSession } from '../../lib/api/customer'; +import getRootUrl from '../../lib/api/getRootUrl'; + +import notify from '../../lib/notifier'; + +const dev = process.env.NODE_ENV !== 'production'; + +// console.log(process.env.Stripe_Test_PublishableKey); + +const stripePromise = loadStripe( + dev ? process.env.Stripe_Test_PublishableKey : process.env.Stripe_Live_PublishableKey, +); +const ROOT_URL = getRootUrl(); + +const styleBuyButton = { + margin: '10px 20px 0px 0px', + font: '14px Roboto', +}; + +class BuyButton extends React.PureComponent { + componentDidMount() { + if (this.props.redirectToCheckout) { + this.handleCheckoutClick(); + } + } + + handleCheckoutClick = async () => { + NProgress.start(); + + try { + const { book } = this.props; + const { sessionId } = await fetchCheckoutSession({ + bookId: book._id, + redirectUrl: document.location.pathname, + }); + + // When the customer clicks on the button, redirect them to Checkout. + const stripe = await stripePromise; + const { error } = await stripe.redirectToCheckout({ sessionId }); + + if (error) { + notify(error); + } + } catch (err) { + notify(err); + } finally { + NProgress.done(); + } + }; + + onLoginClicked = () => { + const { user } = this.props; + + if (!user) { + const redirectUrl = `${window.location.pathname}?buy=1`; + window.location.href = `${ROOT_URL}/auth/google?redirectUrl=${redirectUrl}`; + } + }; + + render() { + const { book, user } = this.props; + + // console.log(redirectToCheckout); + + if (!book) { + return null; + } + + if (!user) { + return ( +
+ +

{book.textNearButton}

+
+
+ ); + } + return ( +
+ +

{book.textNearButton}

+
+
+ ); + } +} + +BuyButton.propTypes = { + book: PropTypes.shape({ + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + slug: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + textNearButton: PropTypes.string, + }), + user: PropTypes.shape({ + _id: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + }), + redirectToCheckout: PropTypes.bool, +}; + +BuyButton.defaultProps = { + book: null, + user: null, + redirectToCheckout: false, +}; + +export default BuyButton; diff --git a/book/9-begin/lib/api/admin.js b/book/9-begin/lib/api/admin.js new file mode 100644 index 00000000..56e384ee --- /dev/null +++ b/book/9-begin/lib/api/admin.js @@ -0,0 +1,40 @@ +import sendRequest from './sendRequest'; + +const BASE_PATH = '/api/v1/admin'; + +export const getBookList = () => + sendRequest(`${BASE_PATH}/books`, { + method: 'GET', + }); + +export const addBook = ({ name, price, githubRepo }) => + sendRequest(`${BASE_PATH}/books/add`, { + body: JSON.stringify({ name, price, githubRepo }), + }); + +export const editBook = ({ id, name, price, githubRepo }) => + sendRequest(`${BASE_PATH}/books/edit`, { + body: JSON.stringify({ + id, + name, + price, + githubRepo, + }), + }); + +export const getBookDetail = ({ slug }) => + sendRequest(`${BASE_PATH}/books/detail/${slug}`, { + method: 'GET', + }); + +// github methods + +export const syncBookContent = ({ bookId }) => + sendRequest(`${BASE_PATH}/books/sync-content`, { + body: JSON.stringify({ bookId }), + }); + +export const getGithubRepos = () => + sendRequest(`${BASE_PATH}/github/repos`, { + method: 'GET', + }); diff --git a/book/9-begin/lib/api/customer.js b/book/9-begin/lib/api/customer.js new file mode 100644 index 00000000..a0ff4760 --- /dev/null +++ b/book/9-begin/lib/api/customer.js @@ -0,0 +1,14 @@ +import sendRequest from './sendRequest'; + +const BASE_PATH = '/api/v1/customer'; + +export const getMyBookList = (options = {}) => + sendRequest(`${BASE_PATH}/my-books`, { + method: 'GET', + ...options, + }); + +export const fetchCheckoutSession = ({ bookId, redirectUrl }) => + sendRequest(`${BASE_PATH}/stripe/fetch-checkout-session`, { + body: JSON.stringify({ bookId, redirectUrl }), + }); diff --git a/book/9-begin/lib/api/getRootUrl.js b/book/9-begin/lib/api/getRootUrl.js new file mode 100644 index 00000000..4b01647d --- /dev/null +++ b/book/9-begin/lib/api/getRootUrl.js @@ -0,0 +1,9 @@ +function getRootUrl() { + const port = process.env.PORT || 8000; + const dev = process.env.NODE_ENV !== 'production'; + const ROOT_URL = dev ? `http://localhost:${port}` : 'https://builderbook.org'; + + return ROOT_URL; +} + +module.exports = getRootUrl; diff --git a/book/9-begin/lib/api/public.js b/book/9-begin/lib/api/public.js new file mode 100644 index 00000000..253fb699 --- /dev/null +++ b/book/9-begin/lib/api/public.js @@ -0,0 +1,19 @@ +import sendRequest from './sendRequest'; + +const BASE_PATH = '/api/v1/public'; + +export const getBookList = () => + sendRequest(`${BASE_PATH}/books`, { + method: 'GET', + }); + +export const getBookDetail = ({ slug }) => + sendRequest(`${BASE_PATH}/books/${slug}`, { + method: 'GET', + }); + +export const getChapterDetail = ({ bookSlug, chapterSlug }, options = {}) => + sendRequest(`${BASE_PATH}/get-chapter-detail?bookSlug=${bookSlug}&chapterSlug=${chapterSlug}`, { + method: 'GET', + ...options, + }); diff --git a/book/9-begin/lib/api/sendRequest.js b/book/9-begin/lib/api/sendRequest.js new file mode 100644 index 00000000..37868792 --- /dev/null +++ b/book/9-begin/lib/api/sendRequest.js @@ -0,0 +1,21 @@ +import 'isomorphic-unfetch'; +import getRootUrl from './getRootUrl'; + +export default async function sendRequest(path, options = {}) { + const headers = { ...(options.headers || {}), 'Content-type': 'application/json; charset=UTF-8' }; + + const response = await fetch(`${getRootUrl()}${path}`, { + method: 'POST', + credentials: 'same-origin', + ...options, + headers, + }); + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + return data; +} diff --git a/book/9-begin/lib/env.js b/book/9-begin/lib/env.js new file mode 100644 index 00000000..5a4c4aaf --- /dev/null +++ b/book/9-begin/lib/env.js @@ -0,0 +1 @@ +export default typeof window !== 'undefined' ? window.__ENV__ : process.env; diff --git a/book/9-begin/lib/notifier.js b/book/9-begin/lib/notifier.js new file mode 100644 index 00000000..b86f5ce9 --- /dev/null +++ b/book/9-begin/lib/notifier.js @@ -0,0 +1,5 @@ +import { openSnackbar } from '../components/Notifier'; + +export default function notify(obj) { + openSnackbar({ message: obj.message || obj.toString() }); +} diff --git a/book/9-begin/lib/theme.js b/book/9-begin/lib/theme.js new file mode 100644 index 00000000..0d77fdaf --- /dev/null +++ b/book/9-begin/lib/theme.js @@ -0,0 +1,13 @@ +import { createMuiTheme } from '@material-ui/core/styles'; +import blue from '@material-ui/core/colors/blue'; +import grey from '@material-ui/core/colors/grey'; + +const theme = createMuiTheme({ + palette: { + primary: { main: blue[700] }, + secondary: { main: grey[700] }, + type: 'light', + }, +}); + +export { theme }; diff --git a/book/9-begin/lib/withAuth.js b/book/9-begin/lib/withAuth.js new file mode 100644 index 00000000..930d9fed --- /dev/null +++ b/book/9-begin/lib/withAuth.js @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Router from 'next/router'; + +let globalUser = null; + +export default function withAuth( + BaseComponent, + { loginRequired = true, logoutRequired = false, adminRequired = false } = {}, +) { + class App extends React.PureComponent { + static async getInitialProps(ctx) { + const isFromServer = !!ctx.req; + const user = ctx.req ? ctx.req.user && ctx.req.user.toObject() : globalUser; + + if (isFromServer && user) { + user._id = user._id.toString(); + } + + const props = { user, isFromServer }; + + if (BaseComponent.getInitialProps) { + Object.assign(props, (await BaseComponent.getInitialProps(ctx)) || {}); + } + + return props; + } + + componentDidMount() { + const { user, isFromServer } = this.props; + + if (isFromServer) { + globalUser = user; + } + + if (loginRequired && !logoutRequired && !user) { + Router.push('/public/login', '/login'); + return; + } + + if (adminRequired && (!user || !user.isAdmin)) { + Router.push('/'); + } + + if (logoutRequired && user) { + Router.push('/'); + } + } + + render() { + const { user } = this.props; + + if (loginRequired && !logoutRequired && !user) { + return null; + } + + if (adminRequired && (!user || !user.isAdmin)) { + return null; + } + + if (logoutRequired && user) { + return null; + } + + return ( + <> + + + ); + } + } + + const propTypes = { + user: PropTypes.shape({ + id: PropTypes.string, + isAdmin: PropTypes.bool, + }), + isFromServer: PropTypes.bool.isRequired, + }; + + const defaultProps = { + user: null, + }; + + App.propTypes = propTypes; + App.defaultProps = defaultProps; + + return App; +} diff --git a/book/9-begin/next.config.js b/book/9-begin/next.config.js new file mode 100644 index 00000000..185fba38 --- /dev/null +++ b/book/9-begin/next.config.js @@ -0,0 +1,9 @@ +require('dotenv').config(); + +module.exports = { + env: { + Stripe_Test_PublishableKey: process.env.Stripe_Test_PublishableKey, + Stripe_Live_PublishableKey: process.env.Stripe_Live_PublishableKey, + GA_MEASUREMENT_ID: process.env.GA_MEASUREMENT_ID, + }, +}; diff --git a/book/9-begin/package.json b/book/9-begin/package.json new file mode 100644 index 00000000..7ff3065c --- /dev/null +++ b/book/9-begin/package.json @@ -0,0 +1,67 @@ +{ + "name": "9-begin", + "version": "0.0.1", + "license": "MIT", + "scripts": { + "dev": "nodemon server/app.js --watch server", + "build": "NODE_ENV=production next build", + "start": "node server/app.js", + "lint": "eslint components pages lib server", + "test": "jest --coverage" + }, + "jest": { + "coverageDirectory": "./.coverage" + }, + "dependencies": { + "@material-ui/core": "4.11.0", + "@material-ui/styles": "4.10.0", + "@octokit/oauth-login-url": "^2.1.2", + "@octokit/rest": "^18.0.3", + "@stripe/stripe-js": "^1.9.0", + "aws-sdk": "2.737.0", + "compression": "1.7.4", + "connect-mongo": "3.2.0", + "dotenv": "8.2.0", + "email-addresses": "3.1.0", + "express": "4.17.1", + "express-session": "1.17.1", + "front-matter": "4.0.2", + "googleapis": "59.0.0", + "handlebars": "4.7.6", + "he": "1.2.0", + "helmet": "4.1.0-rc.2", + "highlight.js": "10.1.2", + "htmlescape": "1.1.1", + "isomorphic-unfetch": "^3.0.0", + "lodash": "4.17.20", + "marked": "1.1.1", + "mongoose": "5.10.0", + "next": "9.1.2", + "node-fetch": "^2.6.0", + "nprogress": "0.2.0", + "passport": "0.4.1", + "passport-google-oauth": "2.0.0", + "prop-types": "15.7.2", + "qs": "6.9.4", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-stripe-checkout": "2.6.3", + "sitemap": "6.2.0", + "stripe": "8.89.0", + "winston": "3.3.3" + }, + "devDependencies": { + "babel-eslint": "10.0.3", + "eslint": "6.7.2", + "eslint-config-airbnb": "18.2.0", + "eslint-config-prettier": "6.11.0", + "eslint-plugin-import": "2.22.0", + "eslint-plugin-jsx-a11y": "6.3.1", + "eslint-plugin-prettier": "3.1.4", + "eslint-plugin-react": "7.20.6", + "eslint-plugin-react-hooks": "4.1.0", + "jest": "26.4.1", + "nodemon": "2.0.4", + "prettier": "2.0.5" + } +} diff --git a/book/9-begin/pages/_app.js b/book/9-begin/pages/_app.js new file mode 100644 index 00000000..8e11ca0c --- /dev/null +++ b/book/9-begin/pages/_app.js @@ -0,0 +1,66 @@ +import CssBaseline from '@material-ui/core/CssBaseline'; +import { ThemeProvider } from '@material-ui/styles'; +import App from 'next/app'; +import React from 'react'; +import Router from 'next/router'; +import NProgress from 'nprogress'; + +import { theme } from '../lib/theme'; + +import Notifier from '../components/Notifier'; +import Header from '../components/Header'; + +Router.events.on('routeChangeStart', () => { + NProgress.start(); +}); + +Router.events.on('routeChangeComplete', (url) => { + if (window && process.env.GA_MEASUREMENT_ID) { + window.gtag('config', process.env.GA_MEASUREMENT_ID, { + page_path: url, + }); + } + + NProgress.done(); +}); + +Router.events.on('routeChangeError', () => NProgress.done()); + +class MyApp extends App { + static async getInitialProps({ Component, ctx }) { + const pageProps = {}; + + if (Component.getInitialProps) { + Object.assign(pageProps, await Component.getInitialProps(ctx)); + } + + return { pageProps }; + } + + componentDidMount() { + // Remove the server-side injected CSS. + const jssStyles = document.querySelector('#jss-server-side'); + if (jssStyles && jssStyles.parentNode) { + jssStyles.parentNode.removeChild(jssStyles); + } + } + + render() { + const { Component, pageProps } = this.props; + + // console.log(pageProps); + + return ( + + {/* ThemeProvider makes the theme available down the React tree thanks to React context. */} + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + {pageProps.chapter ? null :
} + + + + ); + } +} + +export default MyApp; diff --git a/book/9-begin/pages/_document.js b/book/9-begin/pages/_document.js new file mode 100644 index 00000000..03d8c83f --- /dev/null +++ b/book/9-begin/pages/_document.js @@ -0,0 +1,185 @@ +/* eslint-disable react/no-danger */ +import React from 'react'; +import Document, { Head, Html, Main, NextScript } from 'next/document'; +import { ServerStyleSheets } from '@material-ui/styles'; +// import htmlescape from 'htmlescape'; + +// const { StripePublishableKey } = process.env; +// // console.log(StripePublishableKey); + +// const env = { StripePublishableKey }; +// // console.log(env); + +class MyDocument extends Document { + static getInitialProps = async (ctx) => { + // Render app and page and get the context of the page with collected side effects. + const sheets = new ServerStyleSheets(); + const originalRenderPage = ctx.renderPage; + + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App) => (props) => sheets.collect(), + }); + + const initialProps = await Document.getInitialProps(ctx); + + return { + ...initialProps, + // Styles fragment is rendered after the app and page rendering finish. + styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], + }; + }; + + render() { + return ( + + + + + + + + + + + + + + +