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]`
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 ================================================ React Router 5 Course
================================================ 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 (

Welcome to Firebase, almost...

This mock application will demonstrate React Router with nested layouts and a strategy for authenticated (protected) routes.

The username is react and the password is react

{errorMessage &&

{errorMessage}

}
) } 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 = () => ( ) ReactDOM.render(, 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 (

Almost Firebase!

Tools from Google for developing great apps, engaging with
your users, and earning more through mobile ads.

Learn More Documentation Support

{ return (

Recent projects

Add Project {Array.isArray(projects) && projects.map(project => (
history.push(`/projects/${project.id}`)}>

{project.name}

{project.id}
))}
) }} />
) } 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 }) => (
) 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 (
) } 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 }) => (
) 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 (
setFormIsDirty(true)} type="text" placeholder="Project Name" required />
Cancel
) } 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 (

{projectContext.name} Blaze plan

Add app
{/* The extra height of the header and the negative margin of this div pulls the panel up into the blue area of the header */}

Develop

Hosting Realtime Database
) } 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 ( {match.url === location.pathname && } Users Sign-in method Templates null} /> ) } export default AuthenticationLayout ================================================ FILE: src/projects/authentication/SignInMethods.js ================================================ import React from 'react' import Card from '../../ui/Card' const SigninMethods = () => { return Sign-in Methods } export default SigninMethods ================================================ FILE: src/projects/authentication/Templates.js ================================================ import React from 'react' import Card from '../../ui/Card' const Templates = () => { return Templates } export default Templates ================================================ FILE: src/projects/authentication/Users.js ================================================ import React from 'react' import Card from '../../ui/Card' const Users = () => { return Users } export default Users ================================================ FILE: src/projects/database/Data.js ================================================ import React from 'react' import Card from '../../ui/Card' const Data = ({ match }) => { return Data ({match.params.databaseType}) } export default Data ================================================ FILE: src/projects/database/DataIndexes.js ================================================ import React from 'react' import Card from '../../ui/Card' const DataIndexes = ({ match }) => { return Indexes ({match.params.databaseType}) } 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 (

Cloud Firestore

View

Realtime Database

View
) } 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 ( {match.url === location.pathname && } Data Rules Indexes null} /> ) } export default DatabaseLayout ================================================ FILE: src/projects/database/Rules.js ================================================ import React from 'react' import Card from '../../ui/Card' const Rules = ({ match }) => { return Rules ({match.params.databaseType}) } 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
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 (
Firebase Logo Firebase
) } export default withRouter(AuthorizedPrimaryHeader) ================================================ FILE: src/ui/Card.js ================================================ import React from 'react' import classnames from 'classnames' const Card = ({ children, className, ...rest }) => (
{children}
) 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 }) => (
{title &&

{title}

} {children}
) 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 }) =>
{children}
// Just a wrapper around NavLink export const Tab = ({ ...rest }) => ================================================ FILE: src/ui/Panel.js ================================================ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' const Panel = ({ children, useMaxWidth, className }) => (
{children}
) 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 }) => ( ) 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 (
{React.Children.map(children, child => (
{child}
))}
) } 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 {children} } 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
Loading...
if (logged !== true) return return } 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 with one: const Router = ({ children }) => ( pathname !== '/' && pathname.slice(-1) === '/' ? ( ) : ( children ) } /> ) 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 = () => (
) const AuthorizedLayout = () => (
Dashboard Products Logout
) const UnauthorizedLayout = () => (
Login
) const DashboardLayout = () => (
) const ProductsLayout = () => (
) const SettingsPage = () => (
) const SearchResultsPage = () => (
) 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); } }