master c27c2e37826f cached
64 files
48.6 KB
14.3k tokens
7 symbols
1 requests
Download .txt
Repository: ReactTraining/react-router-5-course
Branch: master
Commit: c27c2e37826f
Files: 64
Total size: 48.6 KB

Directory structure:
gitextract_xcgoxx4g/

├── .babelrc
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── config/
│   ├── env.js
│   ├── setup.js
│   ├── webpack.config.dev.js
│   ├── webpack.config.production.js
│   └── webpack.devserver.config.js
├── getbranches.sh
├── package.json
├── public/
│   ├── index.html
│   └── static/
│       └── css/
│           └── main.css
└── src/
    ├── auth/
    │   └── Login.js
    ├── database.json
    ├── index.js
    ├── layouts/
    │   ├── AccountSubLayout.js
    │   ├── AuthorizedLayout.js
    │   ├── ProjectSubLayout.js
    │   └── UnauthorizedLayout.js
    ├── projects/
    │   ├── AddProject.js
    │   ├── Overview.js
    │   ├── authentication/
    │   │   ├── AuthenticationLayout.js
    │   │   ├── SignInMethods.js
    │   │   ├── Templates.js
    │   │   └── Users.js
    │   └── database/
    │       ├── Data.js
    │       ├── DataIndexes.js
    │       ├── DatabaseHome.js
    │       ├── DatabaseLayout.js
    │       └── Rules.js
    ├── styles/
    │   ├── layouts/
    │   │   ├── account-sub-layout.scss
    │   │   ├── app.scss
    │   │   └── project-sub-layout.scss
    │   ├── main.scss
    │   ├── mixins/
    │   │   └── layout.scss
    │   ├── reset.scss
    │   ├── ui/
    │   │   ├── authorized-primary-header.scss
    │   │   ├── button.scss
    │   │   ├── card.scss
    │   │   ├── input-fields.scss
    │   │   ├── page-header.scss
    │   │   ├── panels/
    │   │   │   ├── panel.scss
    │   │   │   ├── recent-projects.scss
    │   │   │   └── welcome-to-firebase.scss
    │   │   ├── project-sidebar.scss
    │   │   └── tiles.scss
    │   ├── utilities.scss
    │   └── vars.scss
    ├── ui/
    │   ├── AuthorizedPrimaryHeader.js
    │   ├── Card.js
    │   ├── PageHeader.js
    │   ├── PageHeaderTabs.js
    │   ├── Panel.js
    │   ├── ProjectSidebar.js
    │   └── Tiles.js
    ├── utils/
    │   ├── AuthUser.js
    │   ├── AuthorizedRoute.js
    │   ├── ProjectContext.js
    │   ├── Router.js
    │   └── api.js
    └── wireframe/
        ├── App.js
        └── wireframes.scss

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}


================================================
FILE: .eslintrc
================================================
{
  "parser": "babel-eslint",
  "extends": ["eslint:recommended", "plugin:react/recommended"],
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "rules": {
    "react/prop-types": "off",
    "no-console": "off",
    "react/no-children-prop": "off",
    "jsx-quotes": ["error", "prefer-double"],
    "react/no-unescaped-entities": "off",
    "no-unused-vars": "off"
  }
}


================================================
FILE: .gitignore
================================================
*.log
.env
.env.production
node_modules
build
npm-debug.log


================================================
FILE: .prettierrc
================================================
tabWidth: 2
semi: false
singleQuote: true
printWidth: 120
trailingComma: true
jsxBracketSameLine: true

================================================
FILE: README.md
================================================
# React Router 5 Course Material

React Router 5 is out and we have created this course so you can learn all about it. The course covers basic and advanced topics. We'll be building a Firebase-looking app that has complex nested layouts and interesting problems to solve.

![Animated Demo](./firebase.gif)

## What's New?

If you're wondering what's new in React Router 5? Not a lot, except internal optimizations and fixes for React 16.x. So you could also think of this as a React Router 4 course since the API is the same. [See more info on the React Router 5 release](https://reacttraining.com/blog/react-router-v5/).

## Download and Install

After downloading the repository from Github, enter the following commands into your command line from the project folder:

```bash
npm install
npm start
```

Then go to [localhost:8000](http://localhost:8000)

> Be sure to see notes on Lesson Branches below...

## Each Lesson is a Git Branch

To view the code for a given lesson, _checkout_ the appropriate branch name. The branch will have the finished code from that lesson.

### Installing lesson branches

All the branches are checked out to your local machine automatically when you do `npm install`. Just do a `git branch` to verify and see all branches after. If they didn't appear, try running `npm run branches` to download all the branches.

To view a branch: `git checkout [branch-name]`

<hr />

Branch names are minimal for easy typing:

- **01-basics** - JSX Routing with `BrowserRouter` and `Route`
- **02-basics** - Route Matching - Inclusive vs Exclusive (exact) and Switch
- **03-basics** - `Link` (anchors)
- **04-basics** - `BrowserRouter` vs `HashRouter`
- **05-basics** - Dynamic (Parameter) Matching
- **06-basics** - Nested Layout Strategy
- **07-basics** - `match.url`
- **08-basics** - `match.path`
- **09-basics** - `NavLink`
- **10-basics** - `withRouter` HoC
- **11-basics** - Programmatic Navigation (History Object)
- **12-basics** - URL Query Strings
- **13-advanced** - Route Render Methods
- **14-advanced** - React Router - Just Components ™
- **15-advanced** - Authentication Strategy with Context
- **16-advanced** - Authenticated Routes (Dynamic Routes)
- **17-advanced** - Navigation With State
- **18-advanced** - Prompt Before Route Changes
- **19-advanced** - Animating Route Changes (Part One)
- **20-advanced** - Animating Route Changes (Part Two)

## Fake Database

Just so we can mimic some data and pretend it's asynchronous, there's a `src/database.json`. Feel free to add more "Firebase Projects" if you want.

## The CSS

I used `className`. Who cares, it keeps the styling clutter out of the JS files since this is teaching material for routing.

## Code Organization

In `/src` you'll see:

- `layouts` for highly re-usable app-wide layouts
- `styles` for Sass modules
- `ui` is where I like to put re-usable "leaf-types" of components
- `utils` is a catch all for React components that are more utilitarian in nature (and less UI in nature) and other general utils.

Any other folders in `/src` is a section of the site, like `/auth` and `/projects` which correspond to `localhost:8000/auth` etc. I guess I could organize those into a `/pages` folder, but who likes deep nesting anyways?


================================================
FILE: config/env.js
================================================
const fs = require('fs')
const path = require('path')
const React = require('react')
const packageJSON = require('../package.json')

const NODE_ENV = process.env.NODE_ENV
if (!NODE_ENV) {
  throw new Error(
    'The NODE_ENV environment variable is required but was not specified.'
  )
}

// Create list of possible env files
const envPath = path.resolve(process.cwd(), '.env')
const dotenvFiles = [
  `${envPath}.${NODE_ENV}`,
  envPath,
]

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set.
// https://github.com/motdotla/dotenv
dotenvFiles.forEach(dotenvFile => {
  if (fs.existsSync(dotenvFile)) {
    require('dotenv').config({
      path: dotenvFile,
    })
  }
})

// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i

function getClientEnvironment() {
  const raw = Object.keys(process.env)
    .filter(key => REACT_APP.test(key))
    .reduce(
      (env, key) => {
        env[key] = process.env[key]
        return env
      },
      {
        // Useful for determining whether we’re running in production mode.
        // Most importantly, it switches React into the correct mode.
        NODE_ENV: process.env.NODE_ENV || 'development',
        // Determines where static assets are located. In development, Webpack
        // DevServer puts them in a virtual folder called `/static`. But we can
        // customise that with .env for production in situations where our assets
        // might be on a CDN
        STATIC_ASSET_URL: process.env.STATIC_ASSET_URL || '/static',
        REACT_VERSION: React.version,
        APP_VERSION: packageJSON.version
      }
    )

  // Stringify all values so we can feed into Webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce(
      (env, key) => {
        env[key] = JSON.stringify(raw[key])
        return env
      },
      {}
    ),
  }

  return { raw, stringified }
}

module.exports = getClientEnvironment

================================================
FILE: config/setup.js
================================================
/* global process */

if (typeof Promise === 'undefined') {
  // Rejection tracking prevents a common issue where React gets into an
  // inconsistent state due to an error, but it gets swallowed by a Promise,
  // and the user has no idea what causes React's erratic future behavior.
  // Only do this in dev for performance reasons
  if (process.env.NODE_ENV) {
    require('promise/lib/rejection-tracking').enable()
  }
  window.Promise = require('promise/lib/es6-extensions.js')
}


================================================
FILE: config/webpack.config.dev.js
================================================
// Do this as the first thing so that any code reading it knows the right env.
process.env.NODE_ENV = 'development'
const getClientEnvironment = require('./env')
const env = getClientEnvironment()

const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpackDevServerConfig = require('./webpack.devserver.config.js')

module.exports = {
  devServer: webpackDevServerConfig,
  // Enhanced dev support (like correct line numbers on errors)
  devtool: 'source-map',
  entry: [path.resolve(process.cwd(), 'config/setup.js'), path.resolve(process.cwd(), 'src/index.js')],
  output: {
    // This does not produce a real file. It's just the virtual path that is
    // served by WebpackDevServer in development. This is the JS bundle
    // containing code from all our entry points, and the Webpack runtime.
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js',
    // Where to create the build
    path: path.resolve(process.cwd(), 'build'),
    publicPath: '/',
  },
  module: {
    rules: [
      // First, run the linter.
      // It's important to do this before Babel processes the JS.
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/,
        loader: 'eslint-loader',
      },
      // Process JS with Babel
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          options: {
            // This is a feature of `babel-loader` for webpack (not Babel itself).
            // It enables caching results in ./node_modules/.cache/babel-loader/
            // directory for faster rebuilds.
            cacheDirectory: true,
          },
          loader: 'babel-loader',
        },
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader', // creates style nodes from JS strings
          'css-loader', // translates CSS into CommonJS
          'sass-loader', // compiles Sass to CSS, using Node Sass by default
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      // Path to HTML file
      template: './public/index.html',
      // Variables listed here in the configurations for HtmlWebpackPlugin become ejs
      // variables for interpolation in the HTML file, accessible with
      // <%- htmlWebpackPlugin.options.[varName] %>. We put our env variables in for HTML access
      env: env.raw,
    }),
    // Make global variables available to the application. We use this to
    // set process.env vars in the front-end
    new webpack.DefinePlugin(env.stringified),
  ],
  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
}


================================================
FILE: config/webpack.config.production.js
================================================
// Do this as the first thing so that any code reading it knows the right env.
process.env.NODE_ENV = 'production'
const getClientEnvironment = require('./env')
const env = getClientEnvironment()

const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // Don't attempt to continue if there are any errors.
  bail: true,
  entry: [path.resolve(process.cwd(), 'config/setup.js'), path.resolve(process.cwd(), 'src/index.js')],
  output: {
    filename: 'static/js/bundle.js',
    // Where to create the build
    path: path.resolve(process.cwd(), 'build'),
    publicPath: '/',
  },
  module: {
    rules: [
      // First, run the linter.
      // It's important to do this before Babel processes the JS.
      {
        test: /\.js$/,
        enforce: 'pre',
        exclude: /node_modules/,
        loader: 'eslint-loader',
      },
      // Process JS with Babel
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          options: { compact: true },
          loader: 'babel-loader',
        },
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader', // creates style nodes from JS strings
          'css-loader', // translates CSS into CommonJS
          'sass-loader', // compiles Sass to CSS, using Node Sass by default
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      // Path to HTML file
      template: './public/index.html',
      // Minification options
      minify: {
        removeComments: false,
        collapseWhitespace: false,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true,
      },
      hash: true,
      // Variables listed here in the configurations for HtmlWebpackPlugin become ejs
      // variables for interpolation in the HTML file, accessible with
      // <%- htmlWebpackPlugin.options.[varName] %>. We put our env variables in for HTML access
      env: env.raw,
    }),
    // Copy static assets from public/static to build/static
    new CopyWebpackPlugin([
      {
        from: path.resolve(process.cwd(), 'public/static'),
        to: 'static',
      },
    ]),
    // Make global variables available to the application. We use this to
    // set process.env vars in the front-end
    new webpack.DefinePlugin(env.stringified),
  ],
  // Some libraries import Node modules but don't use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  node: {
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
  },
  // Don't show performance hints at build time. They just tell us to use code splitting
  performance: {
    hints: false,
  },
}


================================================
FILE: config/webpack.devserver.config.js
================================================
const path = require('path')

module.exports = {
  // Tell the server where to serve static content from.
  contentBase: path.join(process.cwd(), 'public'),
  // By default files from `contentBase` will not trigger a page reload.
  watchContentBase: true,
  // Open a web browser.
  open: true,
  // Overlay errors in the browser
  overlay: true,
  // Enable gzip compression of generated files.
  compress: true,
  // Port to view dev env (localhost:3010)
  port: 8000,
  // Silence WebpackDevServer's own logs since they're generally not useful.
  // It will still show compile warnings and errors with this setting.
  clientLogLevel: 'none',
  // Show less info in the terminal (errors only)
  stats: "errors-only",
  // Reportedly, this avoids CPU overload on some systems.
  // https://github.com/facebookincubator/create-react-app/issues/293
  watchOptions: {
    ignored: /node_modules/,
  },
  // Since development mode thinks of Webpack DevServer as the place to get index.html
  // instead of a file system or a Node backend, we need to tell DevServer to always
  // serve index.html for refreshes since we have an SPA
  historyApiFallback: true
}


================================================
FILE: getbranches.sh
================================================
for branch in `git branch -a | grep remotes | grep -v HEAD | grep -v master `; do
   git branch --track ${branch#remotes/origin/} $branch
done

================================================
FILE: package.json
================================================
{
  "name": "react-router-course",
  "version": "0.1.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --mode development --config config/webpack.config.dev.js",
    "build": "webpack --mode production --config config/webpack.config.production.js",
    "branches": "sh getbranches.sh",
    "postinstall": "npm run branches"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/preset-env": "^7.2.3",
    "@babel/preset-react": "^7.0.0",
    "babel-eslint": "^8.2.3",
    "babel-loader": "^8.0.4",
    "copy-webpack-plugin": "^4.5.1",
    "css-loader": "^2.1.1",
    "dotenv": "^5.0.1",
    "eslint": "^4.19.1",
    "eslint-loader": "^2.0.0",
    "eslint-plugin-react": "^7.7.0",
    "fs-extra": "^5.0.0",
    "html-webpack-plugin": "^3.2.0",
    "sass": "^1.34.1",
    "sass-loader": "^7.3.1",
    "style-loader": "^0.23.1",
    "webpack": "^4.28.0",
    "webpack-cli": "^3.1.1",
    "webpack-dev-server": "^3.2.1"
  },
  "dependencies": {
    "@babel/runtime": "^7.2.0",
    "classnames": "^2.2.6",
    "js-cookie": "^2.2.0",
    "promise": "^8.0.3",
    "prop-types": "^15.6.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-flex-columns": "^0.4.2",
    "react-router-dom": "^5.1.2",
    "react-transition-group": "^4.1.1"
  }
}


================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>React Router 5 Course</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,300" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>


================================================
FILE: public/static/css/main.css
================================================
/* Empty */

================================================
FILE: src/auth/Login.js
================================================
import React, { useState } from 'react'
import Panel from '../ui/Panel'
import Card from '../ui/Card'
import { useAuthUser } from '../utils/AuthUser'

// Fake API Network Call
const apiLogin = (username, password) => {
  return new Promise((resolve, reject) => {
    if (username === 'react' && password === 'react') {
      resolve()
    } else {
      reject()
    }
  })
}

const Login = ({ history }) => {
  const { setLogged } = useAuthUser()
  const [errorMessage, setErrorMessage] = useState()

  function handleSubmit(e) {
    e.preventDefault()
    const [usernameNode, passwordNode] = e.target.elements

    apiLogin(usernameNode.value, passwordNode.value)
      .then(() => {
        setLogged(true)
        history.push('/projects')
      })
      .catch(() => {
        setLogged(false)
        setErrorMessage('Invalid')
      })
  }

  return (
    <Panel>
      <Card style={{ minHeight: '10em' }}>
        <h1 className="heading-2">Welcome to Firebase, almost...</h1>
        <p>
          This mock application will demonstrate React Router with nested layouts and a strategy for authenticated
          (protected) routes.
        </p>
        <p>
          The username is <strong>react</strong> and the password is <strong>react</strong>
        </p>
        {errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
        <form className="spacing" onSubmit={handleSubmit}>
          <input type="text" placeholder="Username" required />
          <input type="password" placeholder="Password" required />
          <button type="submit" className="button">
            Login
          </button>
        </form>
      </Card>
    </Panel>
  )
}

export default Login


================================================
FILE: src/database.json
================================================
{
  "projects": [
    {
      "id": "react-training",
      "name": "ReactTraining.com"
    },
    {
      "id": "secret-project",
      "name": "Secret Project"
    }
  ]
}


================================================
FILE: src/index.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import { Switch, Route, Redirect } from 'react-router-dom'
import Router from './utils/Router'
import { AuthUserProvider } from './utils/AuthUser'
import AuthorizedRoute from './utils/AuthorizedRoute'
import UnauthorizedLayout from './layouts/UnauthorizedLayout'
import AuthorizedLayout from './layouts/AuthorizedLayout'
import './styles/main.scss'

const App = () => (
  <Router>
    <AuthUserProvider>
      <Switch>
        <Route path="/auth" component={UnauthorizedLayout} />
        <AuthorizedRoute path="/projects" component={AuthorizedLayout} />
        <Redirect to="/projects" />
      </Switch>
    </AuthUserProvider>
  </Router>
)

ReactDOM.render(<App />, document.getElementById('root'))


================================================
FILE: src/layouts/AccountSubLayout.js
================================================
import React, { useEffect, useState, Fragment } from 'react'
import { Link, Route } from 'react-router-dom'
import Panel from '../ui/Panel'
import Tiles from '../ui/Tiles'
import Card from '../ui/Card'
import AddProject from '../projects/AddProject'
import { getProjects } from '../utils/api'

const AccountSubLayout = ({ match, history }) => {
  const [projects, setProjects] = useState(false)

  useEffect(() => {
    let isCurrent = true
    getProjects().then(projects => {
      if (isCurrent) {
        setProjects(projects)
      }
    })
    return () => (isCurrent = false)
  }, [])

  return (
    <div className="account-sub-layout">
      <Panel className="panel-welcome-to-firebase">
        <h1 className="heading-1">Almost Firebase!</h1>
        <p>
          Tools from Google for developing great apps, engaging with
          <br />
          your users, and earning more through mobile ads.
        </p>
        <p className="horizontal-spacing">
          <a href="#">Learn More</a>
          <a href="#">Documentation</a>
          <a href="#">Support</a>
        </p>
      </Panel>
      <Panel className="panel-recent-projects">
        <Route path="/projects/add" component={AddProject} />
        <Route
          path="/projects"
          exact
          render={() => {
            return (
              <Fragment>
                <p>Recent projects</p>
                <div>
                  <Tiles>
                    <Card className="card-recent-project center-blocks" style={{ height: '14em', cursor: 'pointer' }}>
                      <Link className="block" to="/projects/add">
                        Add Project
                      </Link>
                    </Card>
                    {Array.isArray(projects) &&
                      projects.map(project => (
                        <div role="link" key={project.id} onClick={() => history.push(`/projects/${project.id}`)}>
                          <Card
                            className="card-recent-project spacing-small"
                            style={{ height: '14em', cursor: 'pointer' }}>
                            <h1 className="heading-3">{project.name}</h1>
                            <div>{project.id}</div>
                          </Card>
                        </div>
                      ))}
                  </Tiles>
                </div>
              </Fragment>
            )
          }}
        />
      </Panel>
    </div>
  )
}

export default AccountSubLayout


================================================
FILE: src/layouts/AuthorizedLayout.js
================================================
import React from 'react'
import { Switch, Route } from 'react-router-dom'
import AuthorizedPrimaryHeader from '../ui/AuthorizedPrimaryHeader'
import AccountSubLayout from './AccountSubLayout'
import ProjectSubLayout from './ProjectSubLayout'

const AuthorizedLayout = ({ match }) => (
  <div className="app authorized-layout">
    <AuthorizedPrimaryHeader />
    <Switch>
      <Route path="/projects/add" exact component={AccountSubLayout} />
      <Route path="/projects" exact component={AccountSubLayout} />
      <Route path="/projects/:projectId" component={ProjectSubLayout} />
    </Switch>
  </div>
)

export default AuthorizedLayout


================================================
FILE: src/layouts/ProjectSubLayout.js
================================================
import React, { useEffect, useState } from 'react'
import { Switch, Route, Redirect, Link } from 'react-router-dom'
import ProjectContext from '../utils/ProjectContext'
import ProjectSidebar from '../ui/ProjectSidebar'
import Overview from '../projects/Overview'
import { getProject } from '../utils/api'
import AuthenticationLayout from '../projects/authentication/AuthenticationLayout'
import DatabaseHome from '../projects/database/DatabaseHome'
import DatabaseLayout from '../projects/database/DatabaseLayout'

const ProjectSubLayout = ({ match, pathname }) => {
  const [project, setProject] = useState(false)
  const { projectId } = match.params

  useEffect(() => {
    let isCurrent = true
    getProject(projectId).then(project => {
      if (isCurrent) {
        setProject(project)
      }
    })
    return () => (isCurrent = false)
  }, [projectId])

  return (
    <ProjectContext.Provider value={project}>
      <div className="project-sub-layout">
        <ProjectSidebar />
        <div className="project-primary-content">
          <nav className="project-nav horizontal-spacing" style={{ color: 'white' }}>
            <span className="text-light-tint">{project.name}</span>
            <span> : </span>
            <Link
              to={{ pathname: '/projects/add', state: { cancelPathname: location.pathname } }}
              className="text-light-tint">
              Add Project
            </Link>
          </nav>
          <main>
            <Switch>
              <Route path={`${match.path}/overview`} component={Overview} />
              <Route path={`${match.path}/authentication`} component={AuthenticationLayout} />
              <Route path={`${match.path}/database`} exact component={DatabaseHome} />
              <Route path={`${match.path}/database/:databaseType`} component={DatabaseLayout} />
              <Redirect to={`${match.path}/overview`} />
            </Switch>
          </main>
        </div>
      </div>
    </ProjectContext.Provider>
  )
}

export default ProjectSubLayout


================================================
FILE: src/layouts/UnauthorizedLayout.js
================================================
import React from 'react'
import { Switch, Route } from 'react-router-dom'
import Login from '../auth/Login'

const UnauthorizedLayout = ({ match }) => (
  <div className="app unauthorized-layout">
    <Switch>
      <Route path={match.url} component={Login} />
    </Switch>
  </div>
)

export default UnauthorizedLayout


================================================
FILE: src/projects/AddProject.js
================================================
import React, { useState } from 'react'
import { Link, Prompt } from 'react-router-dom'
import Card from '../ui/Card'

const AddProject = ({ location }) => {
  const [formIsDirty, setFormIsDirty] = useState(false)

  function handleSubmit(e) {
    e.preventDefault()
  }

  return (
    <Card style={{ minHeight: '15em' }}>
      <Prompt when={formIsDirty} message="Are you sure you want to leave this form before saving?" />
      <form className="spacing" onSubmit={handleSubmit}>
        <input onChange={() => setFormIsDirty(true)} type="text" placeholder="Project Name" required />
        <footer className="horizontal-spacing">
          <button type="submit" className="button">
            Add Project
          </button>
          <Link to={(location.state && location.state.cancelPathname) || '/projects'}>Cancel</Link>
        </footer>
      </form>
    </Card>
  )
}

export default AddProject


================================================
FILE: src/projects/Overview.js
================================================
import React, { Fragment, useContext } from 'react'
import { Columns, Column } from 'react-flex-columns'
import ProjectContext from '../utils/ProjectContext'
import Panel from '../ui/Panel'
import Card from '../ui/Card'
import PageHeader from '../ui/PageHeader'

const Overview = () => {
  const projectContext = useContext(ProjectContext)

  return (
    <Fragment>
      <PageHeader style={{ height: '25em' }}>
        <h1 className="heading-2 horizontal-spacing">
          <span>{projectContext.name}</span>
          <span className="plan">Blaze plan</span>
        </h1>
        <div>
          <span className="add-app text-light-tint">Add app</span>
        </div>
      </PageHeader>
      {/* The extra height of the header and the negative margin of this div
          pulls the panel up into the blue area of the header
      */}
      <div style={{ marginTop: '-18em' }}>
        <Panel>
          <h2 className="heading-3 text-light-tint">Develop</h2>
          <Columns gutterSize={0.8}>
            <Column flex>
              <Card style={{ height: '20em' }}>Hosting</Card>
            </Column>
            <Column flex>
              <Card style={{ height: '20em' }}>Realtime Database</Card>
            </Column>
          </Columns>
        </Panel>
      </div>
    </Fragment>
  )
}

export default Overview


================================================
FILE: src/projects/authentication/AuthenticationLayout.js
================================================
import React, { Fragment } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import Panel from '../../ui/Panel'
import PageHeader from '../../ui/PageHeader'
import { PageHeaderTabs, Tab } from '../../ui/PageHeaderTabs'
import Users from './Users'
import SigninMethods from './SigninMethods'
import Templates from './Templates'

const AuthenticationLayout = ({ match, location }) => {
  return (
    <Fragment>
      {match.url === location.pathname && <Redirect to={`${match.url}/users`} />}
      <PageHeader title="Authentication" useMaxWidth={false}>
        <PageHeaderTabs>
          <Tab to={`${match.url}/users`}>Users</Tab>
          <Tab to={`${match.url}/signin-method`}>Sign-in method</Tab>
          <Tab to={`${match.url}/templates`}>Templates</Tab>
        </PageHeaderTabs>
      </PageHeader>
      <Panel>
        <TransitionGroup className="animated-cards">
          <CSSTransition key={location.key} timeout={600} classNames="animated-card">
            <Switch location={location}>
              <Route path={`${match.path}/users`} component={Users} />
              <Route path={`${match.path}/signin-method`} component={SigninMethods} />
              <Route path={`${match.path}/templates`} component={Templates} />
              <Route component={() => null} />
            </Switch>
          </CSSTransition>
        </TransitionGroup>
      </Panel>
    </Fragment>
  )
}

export default AuthenticationLayout


================================================
FILE: src/projects/authentication/SignInMethods.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const SigninMethods = () => {
  return <Card style={{ height: '20em' }}>Sign-in Methods</Card>
}

export default SigninMethods


================================================
FILE: src/projects/authentication/Templates.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const Templates = () => {
  return <Card style={{ height: '20em' }}>Templates</Card>
}

export default Templates


================================================
FILE: src/projects/authentication/Users.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const Users = () => {
  return <Card style={{ height: '20em' }}>Users</Card>
}

export default Users


================================================
FILE: src/projects/database/Data.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const Data = ({ match }) => {
  return <Card style={{ height: '20em' }}>Data ({match.params.databaseType})</Card>
}

export default Data


================================================
FILE: src/projects/database/DataIndexes.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const DataIndexes = ({ match }) => {
  return <Card style={{ height: '20em' }}>Indexes ({match.params.databaseType})</Card>
}

export default DataIndexes


================================================
FILE: src/projects/database/DatabaseHome.js
================================================
import React, { Fragment } from 'react'
import { Link } from 'react-router-dom'
import { Columns, Column } from 'react-flex-columns'
import Panel from '../../ui/Panel'
import PageHeader from '../../ui/PageHeader'
import Card from '../../ui/Card'

const DatabaseHome = ({ match }) => {
  return (
    <Fragment>
      <PageHeader title="Database" useMaxWidth={false} />
      <Panel>
        <Columns gutterSize={0.8}>
          <Column flex>
            <Card className="spacing">
              <h1 className="heading-3">Cloud Firestore</h1>
              <div>
                <Link to={`${match.url}/firestore`}>View</Link>
              </div>
            </Card>
          </Column>
          <Column flex>
            <Card className="spacing">
              <h1 className="heading-3">Realtime Database</h1>
              <div>
                <Link to={`${match.url}/realtime`}>View</Link>
              </div>
            </Card>
          </Column>
        </Columns>
      </Panel>
    </Fragment>
  )
}

export default DatabaseHome


================================================
FILE: src/projects/database/DatabaseLayout.js
================================================
import React, { Fragment } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import Panel from '../../ui/Panel'
import PageHeader from '../../ui/PageHeader'
import { PageHeaderTabs, Tab } from '../../ui/PageHeaderTabs'
import Data from './Data'
import Rules from './Rules'
import DataIndexes from './DataIndexes'

const DatabaseLayout = ({ match, location }) => {
  return (
    <Fragment>
      {match.url === location.pathname && <Redirect to={`${match.url}/data`} />}
      <PageHeader title="Database" useMaxWidth={false}>
        <PageHeaderTabs>
          <Tab to={`${match.url}/data`}>Data</Tab>
          <Tab to={`${match.url}/rules`}>Rules</Tab>
          <Tab to={`${match.url}/indexes`}>Indexes</Tab>
        </PageHeaderTabs>
      </PageHeader>
      <Panel>
        <TransitionGroup className="animated-cards">
          <CSSTransition key={location.key} timeout={600} classNames="animated-card">
            <Switch>
              <Route path={`${match.path}/data`} component={Data} />
              <Route path={`${match.path}/rules`} component={Rules} />
              <Route path={`${match.path}/indexes`} component={DataIndexes} />
              <Route component={() => null} />
            </Switch>
          </CSSTransition>
        </TransitionGroup>
      </Panel>
    </Fragment>
  )
}

export default DatabaseLayout


================================================
FILE: src/projects/database/Rules.js
================================================
import React from 'react'
import Card from '../../ui/Card'

const Rules = ({ match }) => {
  return <Card style={{ height: '20em' }}>Rules ({match.params.databaseType})</Card>
}

export default Rules


================================================
FILE: src/styles/layouts/account-sub-layout.scss
================================================
.account-sub-layout {
  min-height: 100vh;

  [class*='heading-'] {
    color: #222;
  }
}


================================================
FILE: src/styles/layouts/app.scss
================================================
.app {
  min-height: 100vh;
}

.app.authorized-layout {
  background-color: $light-gray;
}

.app.unauthorized-layout {
  background-image: url('/static/background.svg');
  background-repeat: no-repeat;
  background-size: cover;
  padding-top: 6em;
}


================================================
FILE: src/styles/layouts/project-sub-layout.scss
================================================
.project-sub-layout {
  min-height: 100vh;
  display: flex;
}

.project-primary-content {
  flex: 1;
  display: flex;
  flex-direction: column;

  main {
    flex: 1;
    overflow-y: scroll;
    padding: 2em;
  }
}

.project-nav {
  font-size: 0.9em;
  background-color: $blue;
  height: $nav-height;
  display: flex;
  align-items: center;
  padding-left: 2em;
  &:hover {
    color: #fff;
  }
}


================================================
FILE: src/styles/main.scss
================================================
// Setup
@import './mixins/layout';
@import './vars';

// Reset and Utilities
@import './reset';
@import './utilities';

// UI
@import './ui/panels/panel';
@import './ui/panels/recent-projects';
@import './ui/panels/welcome-to-firebase';
@import './ui/authorized-primary-header';
@import './ui/button';
@import './ui/card';
@import './ui/input-fields';
@import './ui/page-header';
@import './ui/project-sidebar';
@import './ui/tiles';

// Layout
@import './layouts/app';
@import './layouts/project-sub-layout';
@import './layouts/account-sub-layout';


================================================
FILE: src/styles/mixins/layout.scss
================================================
// Vertical and Horizontal Gutters (Lobotomized Owl)
@mixin spacing($gutter, $horizontal: false) {
  /**
   * Apply to containers to get nice gutters between vertical items
   *
   * More Details: https://www.youtube.com/watch?v=w4skJXB03Ho&noredirect=1
   */

  @if $horizontal {
    // Its the "Lobotomized Owl" horizontally
    > * + * {
      margin-left: $gutter !important;
    }
  } @else {
    // Reset all first-child elements to have no vertical margin
    > * {
      margin-top: 0;
      margin-bottom: 0;
    }

    // Its the "Lobotomized Owl" vertically
    > * + * {
      margin-top: $gutter !important;
    }
  }
}


================================================
FILE: src/styles/reset.scss
================================================
// ****************************************
//   Inherited Styles
// ****************************************

// See this explanation for details
// https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/

html {
  box-sizing: border-box;
  color: #888;
  font-family: 'Roboto';
  font-size: 12pt;
  font-weight: 300;
  line-height: 1.5em;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}

// ****************************************
//   Resets
// ****************************************

body {
  margin: 0;
}

a {
  color: $blue;
  text-decoration: none;
}

h1,
h2,
h3,
h4 {
  font-size: 1em;
  font-weight: 300;
}

p a {
  text-decoration: underline;
}

strong {
  color: #222;
}

/****************************************
  Misc
*****************************************/

// Find class names that are undefined (an error from JS)
.undefined {
  background-color: lime !important;
}


================================================
FILE: src/styles/ui/authorized-primary-header.scss
================================================
.authorized-primary-header {
  position: relative;

  .logo {
    font-size: 18pt;
    color: #777;
    position: absolute;
    top: 0.7rem;
    left: 1rem;

    img {
      height: 1.1em;
      display: inline-block;
      margin-right: 0.5em;
    }
    > * {
      vertical-align: middle;
    }

    &.logo-light {
      color: #fff;
    }
  }

  .primary-nav {
    position: absolute;
    top: 0.5rem;
    right: 1rem;
    // min-width: 10em;

    > * {
      vertical-align: middle;
    }

    .logout {
      color: #fff;
      margin-right: 1em;
    }

    .avatar {
      border-radius: 50%;
      width: 2em;
      height: 2em;
      background-color: rgba(0, 0, 0, 0.2);
      display: inline-block;
    }
  }
}


================================================
FILE: src/styles/ui/button.scss
================================================
a.button {
  text-decoration: none;
}

button {
  color: $blue;
  // font: ${theme.copy.fontFamily};
  &:not(.button):hover {
    text-decoration: underline;
  }
}

.button,
button {
  padding: 0;
  border: none;
  background-color: transparent;
  display: inline-block;
  cursor: pointer;
}

.button {
  background-color: $blue;
  color: #fff;
  letter-spacing: 0.1em;
  padding: 0.7em 1.8em;
  border-radius: 0.4rem;

  &:hover,
  &:focus {
    background-color: darken($blue, 5%);
  }
}


================================================
FILE: src/styles/ui/card.scss
================================================
.card {
  background-color: #fff;
  box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
  border-radius: 0.5em;
  padding: 1em;
  min-height: 3em;
}

.animated-cards {
  position: relative;

  > * {
    position: absolute;
    width: 100%;
  }
}

.animated-card {
  &-exit {
    opacity: 1;
    transition: all 0.6s;
  }
  &-exit-active {
    opacity: 0;
    transform: translateY(3em);
  }

  &-enter {
    opacity: 0;
    transition: all 0.6s;
    transform: translateY(-1em);
  }
  &-enter-active {
    opacity: 1;
    transform: translateY(0);
  }
}


================================================
FILE: src/styles/ui/input-fields.scss
================================================
// Just enough styling to get the job done. Don't take
// this too seriously

[type='text'],
[type='password'] {
  display: block;
  width: 25em;
  padding: 0.5em;
  border: 1px solid $gray;
  &:focus {
    outline: none;
    border-color: $blue;
  }
}


================================================
FILE: src/styles/ui/page-header.scss
================================================
.page-header {
  background-color: $blue;
  color: #fff;

  // Offset padding of <main>
  margin: -2rem -2rem 2rem -2rem;
  padding: 1rem 2rem;

  .plan {
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    font-weight: 300;
    padding: 0.5em 1.2em;
    border-radius: 5em;
    background-color: rgba(#fff, 0.2);
  }

  .add-app {
    border-radius: 5em;
    padding: 0.3em 1em;
    background-color: rgba(#000, 0.1);
  }
}

.page-header-tabs {
  font-size: 0.9em;
  // Offsets padding of container
  margin-bottom: -1rem;
  a {
    display: inline-block;
    border-bottom: 3px solid transparent;
    color: rgba(#fff, 0.6);
    padding: 0.5em 0;

    + a {
      margin-left: 1.5em;
    }

    &.active {
      border-bottom-color: #fff;
      color: #fff;
    }
  }
}


================================================
FILE: src/styles/ui/panels/panel.scss
================================================
.panel {
  &.max-width > div {
    max-width: 60em;
    margin: 0 auto;
  }
}


================================================
FILE: src/styles/ui/panels/recent-projects.scss
================================================
.panel.panel-recent-projects {
  margin-top: -7em;
  padding-bottom: 3em;
}

.card-recent-project {
  min-height: 12em;
}


================================================
FILE: src/styles/ui/panels/welcome-to-firebase.scss
================================================
.panel.panel-welcome-to-firebase {
  background: #fff url('/static/account-background.png');
  background-position: right top;
  background-repeat: no-repeat;
  padding: 8em 0 12em 0;
}


================================================
FILE: src/styles/ui/project-sidebar.scss
================================================
.project-sidebar {
  width: 16em;
  // Offset Nav Height
  padding-top: $nav-height;
  background-color: #262f3e;
  border-left: 1px solid #404854;

  [class*='heading-'] {
    color: #fff;
  }

  nav {
    font-size: 0.8em;
    color: rgba(255, 255, 255, 0.2);
    border-bottom: 1px solid #404854;
    padding: 1.5em;

    &:first-child {
      border-top: 1px solid #404854;
    }

    &.overview a:not(.active) {
      color: #babdc0;
    }

    a.active {
      color: #4fc3f7;
    }

    &.open {
      background-color: #19212b;

      a {
        color: #babdc0;
        font-size: 1.2em;
        display: block;
        margin-left: 1em;
        &.active {
          color: #4fc3f7;
        }
      }
    }

    p {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  }
}


================================================
FILE: src/styles/ui/tiles.scss
================================================
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
  grid-gap: 1.3em;
}


================================================
FILE: src/styles/utilities.scss
================================================
$gutter: 1em;
$gutter-small: 0.6em;

/**
 * Layout
 */

.block {
  display: block;
}

.spacing-small {
  @include spacing(0.2em);
}
.spacing-medium {
  @include spacing($gutter-small);
}
.spacing {
  @include spacing($gutter);
}

.horizontal-spacing-small {
  @include spacing(0.2em, true);
}
.horizontal-spacing-medium {
  @include spacing($gutter-small, true);
}
.horizontal-spacing {
  @include spacing($gutter, true);
}

.vertical-middle > * {
  vertical-align: middle;
}

.left-content {
  text-align: left;
}
.center-content {
  text-align: center;
}
.right-content {
  text-align: right;
}

.center-blocks {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
}

/**
 * Copy
 */

.heading-1,
.heading-2,
.heading-3,
.heading-4 {
  display: block;
}

.heading-1 {
  font-size: 2rem;
}

.heading-2 {
  font-size: 1.6rem;
  font-weight: 400;
}

.heading-3 {
  font-size: 1.3rem;
  font-weight: 400;
}

.heading-4 {
  font-size: 1rem;
  font-weight: 400;
}

.text-light-tint {
  color: rgba(#fff, 0.6);
}


================================================
FILE: src/styles/vars.scss
================================================
// Colors
$light-blue: #039be5;
$blue: #0099e8;
$light-gray: #eceff1;
$gray: #d1d2d3;

// Layout
$nav-height: 50px;


================================================
FILE: src/ui/AuthorizedPrimaryHeader.js
================================================
import React from 'react'
import { Link, withRouter } from 'react-router-dom'
import classnames from 'classnames'
import { useAuthUser } from '../utils/AuthUser'

const AuthorizedPrimaryHeader = ({ location }) => {
  const { setLogged } = useAuthUser()
  const lightBackground = ['/projects', '/projects/add'].includes(location.pathname)

  return (
    <header className="authorized-primary-header">
      <Link to="/projects" className={classnames('logo', { 'logo-light': !lightBackground })}>
        <img src="/static/firebase.svg" alt="Firebase Logo" />
        <span>Firebase</span>
      </Link>
      <nav className="primary-nav">
        <button onClick={() => setLogged(false)} className="logout text-light-tint">
          Logout
        </button>
        <div className="avatar" />
      </nav>
    </header>
  )
}

export default withRouter(AuthorizedPrimaryHeader)


================================================
FILE: src/ui/Card.js
================================================
import React from 'react'
import classnames from 'classnames'

const Card = ({ children, className, ...rest }) => (
  <div className={classnames('card', className)} {...rest}>
    {children}
  </div>
)

export default Card


================================================
FILE: src/ui/PageHeader.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import Panel from '../ui/Panel'

const PageHeader = ({ children, title, useMaxWidth, ...rest }) => (
  <header className="page-header" {...rest}>
    <Panel useMaxWidth={useMaxWidth}>
      {title && <h1 className="heading-2">{title}</h1>}
      {children}
    </Panel>
  </header>
)

PageHeader.defaultProps = {
  useMaxWidth: true,
}

PageHeader.propTypes = {
  useMaxWidth: PropTypes.bool,
}

export default PageHeader


================================================
FILE: src/ui/PageHeaderTabs.js
================================================
import React from 'react'
import { NavLink } from 'react-router-dom'

export const PageHeaderTabs = ({ children }) => <div className="page-header-tabs">{children}</div>

// Just a wrapper around NavLink
export const Tab = ({ ...rest }) => <NavLink activeClassName="active" {...rest} />


================================================
FILE: src/ui/Panel.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'

const Panel = ({ children, useMaxWidth, className }) => (
  <div className={classnames('panel', className, { 'max-width': useMaxWidth })}>
    <div className="spacing">{children}</div>
  </div>
)

Panel.defaultProps = {
  useMaxWidth: true,
}

Panel.propTypes = {
  useMaxWidth: PropTypes.bool,
}

export default Panel


================================================
FILE: src/ui/ProjectSidebar.js
================================================
import React from 'react'
import { NavLink, withRouter } from 'react-router-dom'

const ProjectSidebar = ({ match }) => (
  <aside className="project-sidebar">
    <nav className="overview">
      <NavLink activeClassName="active" to={`${match.url}/overview`} className="heading-4">
        Project Overview
      </NavLink>
    </nav>
    <nav className="open spacing">
      <h1 className="heading-4">Develop</h1>
      <div className="spacing-medium">
        <NavLink activeClassName="active" to={`${match.url}/authentication`}>
          Authentication
        </NavLink>
        <NavLink activeClassName="active" to={`${match.url}/database`}>
          Database
        </NavLink>
        <NavLink activeClassName="active" to={`${match.url}/storage`}>
          Storage
        </NavLink>
        <NavLink activeClassName="active" to={`${match.url}/hosting`}>
          Hosting
        </NavLink>
        <NavLink activeClassName="active" to={`${match.url}/functions`}>
          Functions
        </NavLink>
        <NavLink activeClassName="active" to={`${match.url}/ml-kit`}>
          ML Kit
        </NavLink>
      </div>
    </nav>
    <nav className="spacing-small">
      <h1 className="heading-4">Quality</h1>
      <p>Crashlytics, Performance, Test Lab</p>
    </nav>
    <nav className="spacing-small">
      <h1 className="heading-4">Analytics</h1>
      <p>Dashboard, Events, Conversions, Audiences</p>
    </nav>
    <nav className="spacing-small">
      <h1 className="heading-4">Grow</h1>
      <p>Predictions, A/B Testing, Cloud Messaging</p>
    </nav>
  </aside>
)

export default withRouter(ProjectSidebar)


================================================
FILE: src/ui/Tiles.js
================================================
import React from 'react'

const Tiles = ({ children, ...rest }) => {
  // The reason for wrapping in an arbitrary div is for one, we need to ensure
  // the first child of grid is under our control without affecting the children
  // passed in, but also grid items will grow vertically to the height of their
  // row siblings, we might not want that for the children passed in, but we can
  // do that to our arbitrary div
  return (
    <div {...rest} className="tiles">
      {React.Children.map(children, child => (
        <div>{child}</div>
      ))}
    </div>
  )
}

export default Tiles


================================================
FILE: src/utils/AuthUser.js
================================================
import React, { useState, useContext, useEffect } from 'react'
import Cookies from 'js-cookie'

const AuthUserContext = React.createContext()

// So we don't conflict with your localhost
const cookieName = 'RR5CourseLogged'

export const AuthUserProvider = ({ children }) => {
  const cookieLogged = Cookies.getJSON(cookieName)
  const [logged, setLogged] = useState(cookieLogged ? cookieLogged.logged : false)

  useEffect(() => {
    if (logged) {
      Cookies.set(cookieName, { logged: true })
    } else {
      Cookies.remove(cookieName)
    }
  }, [logged])

  return <AuthUserContext.Provider value={{ logged, setLogged }}>{children}</AuthUserContext.Provider>
}

export const useAuthUser = () => {
  return useContext(AuthUserContext)
}


================================================
FILE: src/utils/AuthorizedRoute.js
================================================
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuthUser } from './AuthUser'

const AuthorizedRoute = ({ component, ...rest }) => {
  const { logged } = useAuthUser()

  if (logged === null) return <div>Loading...</div>
  if (logged !== true) return <Redirect push to="/auth" />
  return <Route component={component} {...rest} />
}

export default AuthorizedRoute


================================================
FILE: src/utils/ProjectContext.js
================================================
import React from 'react'

const ProjectContext = React.createContext()
export default ProjectContext


================================================
FILE: src/utils/Router.js
================================================
import React from 'react'
import { BrowserRouter, Route, Redirect } from 'react-router-dom'

// Normalize all paths to not have trailing slashes even if they
// matched <Route> with one:
const Router = ({ children }) => (
  <BrowserRouter>
    <Route
      render={({ location: { pathname, search, hash } }) =>
        pathname !== '/' && pathname.slice(-1) === '/' ? (
          <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
        ) : (
          children
        )
      }
    />
  </BrowserRouter>
)

export default Router


================================================
FILE: src/utils/api.js
================================================
// Fake little database
import database from '../database.json'

export function getProjects() {
  return Promise.resolve(database.projects)
}

export function getProject(id) {
  const project = database.projects.find(p => p.id === id)
  return Promise.resolve(project || null)
}


================================================
FILE: src/wireframe/App.js
================================================
import React from 'react'
import { BrowserRouter, Switch, Route, Redirect, Link } from 'react-router-dom'

import './wireframes.scss'

const App = () => (
  <BrowserRouter>
    <div className="app">
      <Switch>
        <Route path="/auth" component={UnauthorizedLayout} />
        <Route path="/" component={AuthorizedLayout} />
      </Switch>
    </div>
  </BrowserRouter>
)

const AuthorizedLayout = () => (
  <div className="authorized-layout">
    <header>
      <Link to="/dashboard">Dashboard</Link>
      <Link to="/products">Products</Link>
      <Link to="/auth">Logout</Link>
    </header>
    <div className="content">
      <Route path="/dashboard" component={DashboardLayout} />
      <Route path="/products" component={ProductsLayout} />
    </div>
    <footer />
  </div>
)

const UnauthorizedLayout = () => (
  <div className="unauthorized-layout">
    <main>
      <Link to="/">Login</Link>
    </main>
  </div>
)

const DashboardLayout = () => (
  <div className="dashboard-layout">
    <aside />
    <div className="content">
      <nav>
        <Link to="/dashboard/settings">Settings</Link>
        <Link to="/dashboard/search">Search</Link>
      </nav>
      <main>
        <Route path="/dashboard/settings" component={SettingsPage} />
        <Route path="/dashboard/search" component={SearchResultsPage} />
      </main>
    </div>
  </div>
)

const ProductsLayout = () => (
  <div className="products-layout">
    <main>
      <div />
      <div />
      <div />
      <div />
      <div />
    </main>
  </div>
)

const SettingsPage = () => (
  <div className="settings-page">
    <div />
    <div />
  </div>
)

const SearchResultsPage = () => (
  <div className="search-results-page">
    <div />
    <div />
    <div />
  </div>
)

export default App


================================================
FILE: src/wireframe/wireframes.scss
================================================
.app {
  background-image: linear-gradient(45deg, #fafafa 25%, transparent 25%),
    linear-gradient(-45deg, #fafafa 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #fafafa 75%),
    linear-gradient(-45deg, transparent 75%, #fafafa 75%);
  background-size: 40px 40px;
  background-position: 0 0, 0 20px, 20px -20px, -20px 0px;

  padding: 2em 5em;
}

$blue: #0099e8;
$green: #1cd41c;

@mixin box($color, $top: true, $right: true, $bottom: true, $left: true) {
  @if $top {
    border-top: 5px dashed $color;
  }
  @if $right {
    border-right: 5px dashed $color;
  }
  @if $bottom {
    border-bottom: 5px dashed $color;
  }
  @if $left {
    border-left: 5px dashed $color;
  }
}

.authorized-layout {
  min-height: 50vh;
  > header {
    @include box($blue);
    padding: 2em;
  }
  > footer {
    @include box($blue);
    padding: 2em;
    margin-top: 1em;
  }
  > .content {
    @include box($blue);
    display: flex;
    min-height: 100px;
    padding: 1em;
    margin-top: 1em;
  }
  a + a {
    margin-left: 2em;
  }
}

.unauthorized-layout {
  display: flex;
  padding: 3em;
  main {
    @include box($blue);
    margin: auto;
    width: 50%;
    height: 200px;
    padding: 1em;
  }
}

.dashboard-layout {
  display: flex;
  width: 100%;
  a {
    color: red;
  }
  > aside {
    width: 12em;
    @include box(red);
  }
  > .content {
    flex: 1;
    @include box(red, true, true, true, false);
    > nav {
      @include box(red, false, false, true, false);
      padding: 1em;
    }
    > main {
      padding: 1em;
      min-height: 10em;
    }
  }
}

.products-layout {
  width: 100%;
  main {
    @include box(red);
    padding: 1em;
    overflow: hidden;
    > div {
      @include box(red);
      width: 100px;
      height: 100px;
      float: left;
      margin-right: 1em;
    }
  }
}

.settings-page {
  display: flex;
  padding: 1em;
  @include box($green);
  > div {
    flex: 1;
    min-height: 10em;
  }
  > :first-child {
    @include box($green);
  }
  > :last-child {
    @include box($green, true, true, true, false);
  }
}

.search-results-page {
  > div + div {
    margin-top: 1em;
  }
  > div {
    padding: 2em;
    @include box($green);
  }
}
Download .txt
gitextract_xcgoxx4g/

├── .babelrc
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── config/
│   ├── env.js
│   ├── setup.js
│   ├── webpack.config.dev.js
│   ├── webpack.config.production.js
│   └── webpack.devserver.config.js
├── getbranches.sh
├── package.json
├── public/
│   ├── index.html
│   └── static/
│       └── css/
│           └── main.css
└── src/
    ├── auth/
    │   └── Login.js
    ├── database.json
    ├── index.js
    ├── layouts/
    │   ├── AccountSubLayout.js
    │   ├── AuthorizedLayout.js
    │   ├── ProjectSubLayout.js
    │   └── UnauthorizedLayout.js
    ├── projects/
    │   ├── AddProject.js
    │   ├── Overview.js
    │   ├── authentication/
    │   │   ├── AuthenticationLayout.js
    │   │   ├── SignInMethods.js
    │   │   ├── Templates.js
    │   │   └── Users.js
    │   └── database/
    │       ├── Data.js
    │       ├── DataIndexes.js
    │       ├── DatabaseHome.js
    │       ├── DatabaseLayout.js
    │       └── Rules.js
    ├── styles/
    │   ├── layouts/
    │   │   ├── account-sub-layout.scss
    │   │   ├── app.scss
    │   │   └── project-sub-layout.scss
    │   ├── main.scss
    │   ├── mixins/
    │   │   └── layout.scss
    │   ├── reset.scss
    │   ├── ui/
    │   │   ├── authorized-primary-header.scss
    │   │   ├── button.scss
    │   │   ├── card.scss
    │   │   ├── input-fields.scss
    │   │   ├── page-header.scss
    │   │   ├── panels/
    │   │   │   ├── panel.scss
    │   │   │   ├── recent-projects.scss
    │   │   │   └── welcome-to-firebase.scss
    │   │   ├── project-sidebar.scss
    │   │   └── tiles.scss
    │   ├── utilities.scss
    │   └── vars.scss
    ├── ui/
    │   ├── AuthorizedPrimaryHeader.js
    │   ├── Card.js
    │   ├── PageHeader.js
    │   ├── PageHeaderTabs.js
    │   ├── Panel.js
    │   ├── ProjectSidebar.js
    │   └── Tiles.js
    ├── utils/
    │   ├── AuthUser.js
    │   ├── AuthorizedRoute.js
    │   ├── ProjectContext.js
    │   ├── Router.js
    │   └── api.js
    └── wireframe/
        ├── App.js
        └── wireframes.scss
Download .txt
SYMBOL INDEX (7 symbols across 4 files)

FILE: config/env.js
  constant NODE_ENV (line 6) | const NODE_ENV = process.env.NODE_ENV
  constant REACT_APP (line 34) | const REACT_APP = /^REACT_APP_/i
  function getClientEnvironment (line 36) | function getClientEnvironment() {

FILE: src/auth/Login.js
  function handleSubmit (line 21) | function handleSubmit(e) {

FILE: src/projects/AddProject.js
  function handleSubmit (line 8) | function handleSubmit(e) {

FILE: src/utils/api.js
  function getProjects (line 4) | function getProjects() {
  function getProject (line 8) | function getProject(id) {
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (56K chars).
[
  {
    "path": ".babelrc",
    "chars": 116,
    "preview": "{\n  \"presets\": [\"@babel/preset-env\", \"@babel/preset-react\"],\n  \"plugins\": [\"@babel/plugin-syntax-dynamic-import\"]\n}\n"
  },
  {
    "path": ".eslintrc",
    "chars": 394,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": [\"eslint:recommended\", \"plugin:react/recommended\"],\n  \"env\": {\n    \"browser\":"
  },
  {
    "path": ".gitignore",
    "chars": 60,
    "preview": "*.log\n.env\n.env.production\nnode_modules\nbuild\nnpm-debug.log\n"
  },
  {
    "path": ".prettierrc",
    "chars": 102,
    "preview": "tabWidth: 2\nsemi: false\nsingleQuote: true\nprintWidth: 120\ntrailingComma: true\njsxBracketSameLine: true"
  },
  {
    "path": "README.md",
    "chars": 3241,
    "preview": "# React Router 5 Course Material\n\nReact Router 5 is out and we have created this course so you can learn all about it. T"
  },
  {
    "path": "config/env.js",
    "chars": 2174,
    "preview": "const fs = require('fs')\nconst path = require('path')\nconst React = require('react')\nconst packageJSON = require('../pac"
  },
  {
    "path": "config/setup.js",
    "chars": 485,
    "preview": "/* global process */\n\nif (typeof Promise === 'undefined') {\n  // Rejection tracking prevents a common issue where React "
  },
  {
    "path": "config/webpack.config.dev.js",
    "chars": 2810,
    "preview": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.NODE_ENV = 'development'\ncons"
  },
  {
    "path": "config/webpack.config.production.js",
    "chars": 2963,
    "preview": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.NODE_ENV = 'production'\nconst"
  },
  {
    "path": "config/webpack.devserver.config.js",
    "chars": 1158,
    "preview": "const path = require('path')\n\nmodule.exports = {\n  // Tell the server where to serve static content from.\n  contentBase:"
  },
  {
    "path": "getbranches.sh",
    "chars": 142,
    "preview": "for branch in `git branch -a | grep remotes | grep -v HEAD | grep -v master `; do\n   git branch --track ${branch#remotes"
  },
  {
    "path": "package.json",
    "chars": 1364,
    "preview": "{\n  \"name\": \"react-router-course\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \""
  },
  {
    "path": "public/index.html",
    "chars": 512,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>React Router 5 Course</title>\n    <met"
  },
  {
    "path": "public/static/css/main.css",
    "chars": 11,
    "preview": "/* Empty */"
  },
  {
    "path": "src/auth/Login.js",
    "chars": 1697,
    "preview": "import React, { useState } from 'react'\nimport Panel from '../ui/Panel'\nimport Card from '../ui/Card'\nimport { useAuthUs"
  },
  {
    "path": "src/database.json",
    "chars": 174,
    "preview": "{\n  \"projects\": [\n    {\n      \"id\": \"react-training\",\n      \"name\": \"ReactTraining.com\"\n    },\n    {\n      \"id\": \"secret"
  },
  {
    "path": "src/index.js",
    "chars": 763,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport { Switch, Route, Redirect } from 'react-router-dom'\nim"
  },
  {
    "path": "src/layouts/AccountSubLayout.js",
    "chars": 2499,
    "preview": "import React, { useEffect, useState, Fragment } from 'react'\nimport { Link, Route } from 'react-router-dom'\nimport Panel"
  },
  {
    "path": "src/layouts/AuthorizedLayout.js",
    "chars": 644,
    "preview": "import React from 'react'\nimport { Switch, Route } from 'react-router-dom'\nimport AuthorizedPrimaryHeader from '../ui/Au"
  },
  {
    "path": "src/layouts/ProjectSubLayout.js",
    "chars": 2032,
    "preview": "import React, { useEffect, useState } from 'react'\nimport { Switch, Route, Redirect, Link } from 'react-router-dom'\nimpo"
  },
  {
    "path": "src/layouts/UnauthorizedLayout.js",
    "chars": 322,
    "preview": "import React from 'react'\nimport { Switch, Route } from 'react-router-dom'\nimport Login from '../auth/Login'\n\nconst Unau"
  },
  {
    "path": "src/projects/AddProject.js",
    "chars": 908,
    "preview": "import React, { useState } from 'react'\nimport { Link, Prompt } from 'react-router-dom'\nimport Card from '../ui/Card'\n\nc"
  },
  {
    "path": "src/projects/Overview.js",
    "chars": 1331,
    "preview": "import React, { Fragment, useContext } from 'react'\nimport { Columns, Column } from 'react-flex-columns'\nimport ProjectC"
  },
  {
    "path": "src/projects/authentication/AuthenticationLayout.js",
    "chars": 1531,
    "preview": "import React, { Fragment } from 'react'\nimport { Switch, Route, Redirect } from 'react-router-dom'\nimport { TransitionGr"
  },
  {
    "path": "src/projects/authentication/SignInMethods.js",
    "chars": 187,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst SigninMethods = () => {\n  return <Card style={{ height"
  },
  {
    "path": "src/projects/authentication/Templates.js",
    "chars": 173,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst Templates = () => {\n  return <Card style={{ height: '2"
  },
  {
    "path": "src/projects/authentication/Users.js",
    "chars": 161,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst Users = () => {\n  return <Card style={{ height: '20em'"
  },
  {
    "path": "src/projects/database/Data.js",
    "chars": 197,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst Data = ({ match }) => {\n  return <Card style={{ height"
  },
  {
    "path": "src/projects/database/DataIndexes.js",
    "chars": 214,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst DataIndexes = ({ match }) => {\n  return <Card style={{"
  },
  {
    "path": "src/projects/database/DatabaseHome.js",
    "chars": 1042,
    "preview": "import React, { Fragment } from 'react'\nimport { Link } from 'react-router-dom'\nimport { Columns, Column } from 'react-f"
  },
  {
    "path": "src/projects/database/DatabaseLayout.js",
    "chars": 1437,
    "preview": "import React, { Fragment } from 'react'\nimport { Switch, Route, Redirect } from 'react-router-dom'\nimport { TransitionGr"
  },
  {
    "path": "src/projects/database/Rules.js",
    "chars": 200,
    "preview": "import React from 'react'\nimport Card from '../../ui/Card'\n\nconst Rules = ({ match }) => {\n  return <Card style={{ heigh"
  },
  {
    "path": "src/styles/layouts/account-sub-layout.scss",
    "chars": 91,
    "preview": ".account-sub-layout {\n  min-height: 100vh;\n\n  [class*='heading-'] {\n    color: #222;\n  }\n}\n"
  },
  {
    "path": "src/styles/layouts/app.scss",
    "chars": 250,
    "preview": ".app {\n  min-height: 100vh;\n}\n\n.app.authorized-layout {\n  background-color: $light-gray;\n}\n\n.app.unauthorized-layout {\n "
  },
  {
    "path": "src/styles/layouts/project-sub-layout.scss",
    "chars": 397,
    "preview": ".project-sub-layout {\n  min-height: 100vh;\n  display: flex;\n}\n\n.project-primary-content {\n  flex: 1;\n  display: flex;\n  "
  },
  {
    "path": "src/styles/main.scss",
    "chars": 551,
    "preview": "// Setup\n@import './mixins/layout';\n@import './vars';\n\n// Reset and Utilities\n@import './reset';\n@import './utilities';\n"
  },
  {
    "path": "src/styles/mixins/layout.scss",
    "chars": 633,
    "preview": "// Vertical and Horizontal Gutters (Lobotomized Owl)\n@mixin spacing($gutter, $horizontal: false) {\n  /**\n   * Apply to c"
  },
  {
    "path": "src/styles/reset.scss",
    "chars": 923,
    "preview": "// ****************************************\n//   Inherited Styles\n// ****************************************\n\n// See th"
  },
  {
    "path": "src/styles/ui/authorized-primary-header.scss",
    "chars": 721,
    "preview": ".authorized-primary-header {\n  position: relative;\n\n  .logo {\n    font-size: 18pt;\n    color: #777;\n    position: absolu"
  },
  {
    "path": "src/styles/ui/button.scss",
    "chars": 490,
    "preview": "a.button {\n  text-decoration: none;\n}\n\nbutton {\n  color: $blue;\n  // font: ${theme.copy.fontFamily};\n  &:not(.button):ho"
  },
  {
    "path": "src/styles/ui/card.scss",
    "chars": 587,
    "preview": ".card {\n  background-color: #fff;\n  box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);"
  },
  {
    "path": "src/styles/ui/input-fields.scss",
    "chars": 253,
    "preview": "// Just enough styling to get the job done. Don't take\n// this too seriously\n\n[type='text'],\n[type='password'] {\n  displ"
  },
  {
    "path": "src/styles/ui/page-header.scss",
    "chars": 774,
    "preview": ".page-header {\n  background-color: $blue;\n  color: #fff;\n\n  // Offset padding of <main>\n  margin: -2rem -2rem 2rem -2rem"
  },
  {
    "path": "src/styles/ui/panels/panel.scss",
    "chars": 78,
    "preview": ".panel {\n  &.max-width > div {\n    max-width: 60em;\n    margin: 0 auto;\n  }\n}\n"
  },
  {
    "path": "src/styles/ui/panels/recent-projects.scss",
    "chars": 122,
    "preview": ".panel.panel-recent-projects {\n  margin-top: -7em;\n  padding-bottom: 3em;\n}\n\n.card-recent-project {\n  min-height: 12em;\n"
  },
  {
    "path": "src/styles/ui/panels/welcome-to-firebase.scss",
    "chars": 186,
    "preview": ".panel.panel-welcome-to-firebase {\n  background: #fff url('/static/account-background.png');\n  background-position: righ"
  },
  {
    "path": "src/styles/ui/project-sidebar.scss",
    "chars": 819,
    "preview": ".project-sidebar {\n  width: 16em;\n  // Offset Nav Height\n  padding-top: $nav-height;\n  background-color: #262f3e;\n  bord"
  },
  {
    "path": "src/styles/ui/tiles.scss",
    "chars": 110,
    "preview": ".tiles {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));\n  grid-gap: 1.3em;\n}\n"
  },
  {
    "path": "src/styles/utilities.scss",
    "chars": 1042,
    "preview": "$gutter: 1em;\n$gutter-small: 0.6em;\n\n/**\n * Layout\n */\n\n.block {\n  display: block;\n}\n\n.spacing-small {\n  @include spacin"
  },
  {
    "path": "src/styles/vars.scss",
    "chars": 116,
    "preview": "// Colors\n$light-blue: #039be5;\n$blue: #0099e8;\n$light-gray: #eceff1;\n$gray: #d1d2d3;\n\n// Layout\n$nav-height: 50px;\n"
  },
  {
    "path": "src/ui/AuthorizedPrimaryHeader.js",
    "chars": 879,
    "preview": "import React from 'react'\nimport { Link, withRouter } from 'react-router-dom'\nimport classnames from 'classnames'\nimport"
  },
  {
    "path": "src/ui/Card.js",
    "chars": 223,
    "preview": "import React from 'react'\nimport classnames from 'classnames'\n\nconst Card = ({ children, className, ...rest }) => (\n  <d"
  },
  {
    "path": "src/ui/PageHeader.js",
    "chars": 483,
    "preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport Panel from '../ui/Panel'\n\nconst PageHeader = ({ chil"
  },
  {
    "path": "src/ui/PageHeaderTabs.js",
    "chars": 286,
    "preview": "import React from 'react'\nimport { NavLink } from 'react-router-dom'\n\nexport const PageHeaderTabs = ({ children }) => <d"
  },
  {
    "path": "src/ui/Panel.js",
    "chars": 417,
    "preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport classnames from 'classnames'\n\nconst Panel = ({ child"
  },
  {
    "path": "src/ui/ProjectSidebar.js",
    "chars": 1634,
    "preview": "import React from 'react'\nimport { NavLink, withRouter } from 'react-router-dom'\n\nconst ProjectSidebar = ({ match }) => "
  },
  {
    "path": "src/ui/Tiles.js",
    "chars": 597,
    "preview": "import React from 'react'\n\nconst Tiles = ({ children, ...rest }) => {\n  // The reason for wrapping in an arbitrary div i"
  },
  {
    "path": "src/utils/AuthUser.js",
    "chars": 746,
    "preview": "import React, { useState, useContext, useEffect } from 'react'\nimport Cookies from 'js-cookie'\n\nconst AuthUserContext = "
  },
  {
    "path": "src/utils/AuthorizedRoute.js",
    "chars": 405,
    "preview": "import React from 'react'\nimport { Route, Redirect } from 'react-router-dom'\nimport { useAuthUser } from './AuthUser'\n\nc"
  },
  {
    "path": "src/utils/ProjectContext.js",
    "chars": 102,
    "preview": "import React from 'react'\n\nconst ProjectContext = React.createContext()\nexport default ProjectContext\n"
  },
  {
    "path": "src/utils/Router.js",
    "chars": 543,
    "preview": "import React from 'react'\nimport { BrowserRouter, Route, Redirect } from 'react-router-dom'\n\n// Normalize all paths to n"
  },
  {
    "path": "src/utils/api.js",
    "chars": 280,
    "preview": "// Fake little database\nimport database from '../database.json'\n\nexport function getProjects() {\n  return Promise.resolv"
  },
  {
    "path": "src/wireframe/App.js",
    "chars": 1785,
    "preview": "import React from 'react'\nimport { BrowserRouter, Switch, Route, Redirect, Link } from 'react-router-dom'\n\nimport './wir"
  },
  {
    "path": "src/wireframe/wireframes.scss",
    "chars": 2195,
    "preview": ".app {\n  background-image: linear-gradient(45deg, #fafafa 25%, transparent 25%),\n    linear-gradient(-45deg, #fafafa 25%"
  }
]

About this extraction

This page contains the full source code of the ReactTraining/react-router-5-course GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (48.6 KB), approximately 14.3k tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!