├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs ├── firebase-service-key.gif ├── firebase-setup.gif └── vouch-demo.gif ├── firebase.json ├── firestore.rules ├── functions ├── .gitignore ├── actions │ ├── getResponseCount.js │ └── postResponse.js ├── index.js └── package.json ├── jsconfig.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── safari-pinned-tab.svg └── site.webmanifest └── src ├── App.js ├── App.test.js ├── GlobalStyle.js ├── backend.js ├── components ├── buttons.js ├── cards.js ├── fields.js ├── inputs.js ├── links.js ├── loadingIndicators.js ├── narrowCardLayout.js └── typography.js ├── config.js ├── index.js ├── media ├── logo.svg └── vouch.svg ├── routes ├── 404.js ├── landing.js ├── privacy.js └── thanks.js ├── theme.js └── utils ├── issues.js └── normalizePathname.js /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_FIREBASE_API_KEY= 2 | REACT_APP_FIREBASE_AUTH_DOMAIN= 3 | REACT_APP_FIREBASE_DATABASE_URL= 4 | REACT_APP_FIREBASE_PROJECT_ID= 5 | REACT_APP_FIREBASE_STORAGE_BUCKET= 6 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID= 7 | REACT_APP_FIREBASE_APP_ID= 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-restricted-imports": [ 5 | "error", 6 | { 7 | "paths": [ 8 | { 9 | "name": "styled-components", 10 | "message": "Please import from styled-components/macro." 11 | } 12 | ], 13 | "patterns": [ 14 | "!styled-components/macro" 15 | ] 16 | } 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | firebase-debug.log* 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | /functions/.serviceaccount.json 27 | .firebase 28 | .firebaserc 29 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxBracketSameLine": true, 8 | "parser": "typescript", 9 | "semi": false, 10 | "rcVerbose": false 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Seven Stripes Kabushiki Kaisha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Make Bacon with React & Firebase 2 | ================================ 3 | 4 | This is the companion repository to [React, Firebase & Bacon](https://frontarm.com/bacon) -- a complete guide to building a real-world app with React. 5 | 6 | The repository contains branches for individual steps within the course. In the first few sections, you'll create and deploy a small [landing page](https://vouch.chat) -- with hooks, Create React App and Firebase: 7 | 8 |

10 | 11 | Then, within later steps, you'll add authentication, profiles, payments, modals -- and other features from [vouch.chat](https://beta.vouch.chat). 12 | 13 | 14 | Outline 15 | ------- 16 | 17 | *This outline just includes branches in this repository. You can the [full course outline](https://frontarm.com/courses/react-and-bacon/getting-started/welcome/) at [Frontend Armory](https://frontarm.com).* 18 | 19 | 20 | ### 1. Setting up 21 | 22 | - [step-010](https://github.com/frontarm/react-firebase-bacon/tree/step-010) – Create an app and deploy it 23 | 24 | 25 | ### 2. Basic hooks and data fetching 26 | 27 | - [step-020](https://github.com/frontarm/react-firebase-bacon/tree/step-020) – Fetch and display data with effects 28 | - [step-021](https://github.com/frontarm/react-firebase-bacon/tree/step-021) – Store form state with `useState` 29 | 30 | 31 | ### 3. Forms, errors and state management 32 | 33 | - [step-030](https://github.com/frontarm/react-firebase-bacon/tree/step-030) – Submitting forms 34 | - [step-031](https://github.com/frontarm/react-firebase-bacon/tree/step-031) – Human-readable error messages 35 | - [step-032](https://github.com/frontarm/react-firebase-bacon/tree/step-032) – Client-side validation 36 | - [step-033](https://github.com/frontarm/react-firebase-bacon/tree/step-033) – Hiding resolved errors 37 | 38 | 39 | ### 4. Styles and animations 40 | 41 | - [step-040](https://github.com/frontarm/react-firebase-bacon/tree/step-040) – Refactor with Styled Components 42 | - [step-041](https://github.com/frontarm/react-firebase-bacon/tree/step-041) – Add the Vouch styles 43 | - [step-042](https://github.com/frontarm/react-firebase-bacon/tree/step-042) – Using images 44 | - [step-043](https://github.com/frontarm/react-firebase-bacon/tree/step-043) – Custom focus styles 45 | - [step-044](https://github.com/frontarm/react-firebase-bacon/tree/step-044) – Add `theme.js` 46 | - [step-045](https://github.com/frontarm/react-firebase-bacon/tree/step-045) – Media queries 47 | - [step-046](https://github.com/frontarm/react-firebase-bacon/tree/step-046) – Filesystem structure 48 | - [step-047](https://github.com/frontarm/react-firebase-bacon/tree/step-047) – Animated loading indicators 49 | 50 | 51 | ### 5. Routing and data subscriptions 52 | 53 | - [step-050](https://github.com/frontarm/react-firebase-bacon/tree/step-050) – Browser history and `` components 54 | - [step-051](https://github.com/frontarm/react-firebase-bacon/tree/step-051) – Simple routing and 404 message 55 | - [step-052](https://github.com/frontarm/react-firebase-bacon/tree/step-052) – Cleaning up after effects 56 | - [step-053](https://github.com/frontarm/react-firebase-bacon/tree/step-053) – A `routes` directory 57 | - [step-054](https://github.com/frontarm/react-firebase-bacon/tree/step-054) – Layout component 58 | - [step-055](https://github.com/frontarm/react-firebase-bacon/tree/step-055) – Programmatic navigation 59 | 60 | 61 | ### 6. Build a backend with Firebase 62 | 63 | - [step-060](https://github.com/frontarm/react-firebase-bacon/tree/step-060) – Calling Firebase from React 64 | - [step-061](https://github.com/frontarm/react-firebase-bacon/tree/step-061) – Firebase functions 65 | - [step-062](https://github.com/frontarm/react-firebase-bacon/tree/step-062) – Reduce costs with counters 66 | - [step-063](https://github.com/frontarm/react-firebase-bacon/tree/step-063) – Real-time updates 67 | - [step-064](https://github.com/frontarm/react-firebase-bacon/tree/step-064) – Staging and production builds 68 | 69 | You can see the result of this step online at [vouch.chat](https://vouch.chat). 70 | 71 | 72 | ### 7. Authentication 73 | 74 | *coming soon* 75 | 76 | 77 | ### 8. Context and advanced hooks 78 | 79 | *coming soon* 80 | 81 | 82 | 83 | Installation 84 | ------------ 85 | 86 | If you just want to get the app working without the full walkthrough, here's what to do. 87 | 88 | 89 | ### 1. Firebase app 90 | 91 | Before starting, you'll need a Firebase project. Once you've set it up, you'll need your firebase project's config object, which is available in the Firebase console and looks a little like this: 92 | 93 | ```js 94 | const firebaseConfig = { 95 | apiKey: "qwertyuiopasdfgh_asdfasdfasdfasdfasdfas", 96 | authDomain: "something.firebaseapp.com", 97 | databaseURL: "https://something.firebaseio.com", 98 | projectId: "something", 99 | storageBucket: "", 100 | messagingSenderId: "111111111111", 101 | appId: "1:111111111111:web:1rstarstarstarst" 102 | }; 103 | ``` 104 | 105 | You'll also need to ensure that your project has a *Firestore* database available. To start, you can create it in test mode -- we'll lock it down later. 106 | 107 | Here's the full process to configure your Firebase project and Firestore database: 108 | 109 |

Firebase project setup

111 | 112 | 113 | ### 2. Clone and install 114 | 115 | Once you've got your Firebase config, start by cloning the repository and installing its dependencies 116 | 117 | ```bash 118 | git clone https://github.com/frontarm/react-firebase-bacon.git app 119 | cd app 120 | npm install 121 | cd functions 122 | npm install 123 | cd .. 124 | ``` 125 | 126 | ### 3. Local configuration files 127 | 128 | To actually build and deploy the app, you'll need to create some configuration files: 129 | 130 | ```bash 131 | cp .env.example .env.development.local 132 | cp .env.example .env.production.local 133 | ``` 134 | 135 | These two files provide configuration for your development server and distributable app, respectively. They start out blank, so you'll need to add the relevant parts from your Firebase config. The end will result will look something like this: 136 | 137 | ``` 138 | // .env files 139 | REACT_APP_FIREBASE_API_KEY=qwertyuiopasdfgh_asdfasdfasdfasdfasdfas 140 | REACT_APP_FIREBASE_AUTH_DOMAIN=something.firebaseapp.com 141 | REACT_APP_FIREBASE_DATABASE_URL=https://something.firebaseio.com 142 | REACT_APP_FIREBASE_PROJECT_ID=something 143 | REACT_APP_FIREBASE_STORAGE_BUCKET= 144 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID=111111111111 145 | REACT_APP_FIREBASE_APP_ID=1:111111111111:web:1rstarstarstarst 146 | ``` 147 | 148 | You'll also need to add a `functions/.serviceaccount.json` file, which includes credentials for your *development* Firebase service account -- needed to test your Firebase functions locally. You can download the contents of this file from the Firebase console: 149 | 150 |

Firebase project setup

152 | 153 | Once you've downloaded the service account file, just move it into your `functions` directory and rename it to `.serviceaccount.json`. 154 | 155 | All of these configuration files are listed in `.gitignore`, so you don't need to worry about accidentally pushing any private configuration to a public repository. 156 | 157 | 158 | ### 4. Start the dev server 159 | 160 | You can test your configuration by starting the development server: 161 | 162 | ```bash 163 | npm run start:client 164 | ``` 165 | 166 | This should open a browser window to , where if everything has gone to plan, you'll see the landing page. However, before being able to submit the form, you'll first need to start a server to simulate your Firebase functions locally. 167 | 168 | 169 | ### 5. Set up the Firebase CLI tools 170 | 171 | Before continuing, you'll first need to make sure you have the Firebase CLI tools installed: 172 | 173 | ```bash 174 | npm install -g firebase-tools 175 | ``` 176 | 177 | Once installed, you'll also need to login to your Firebase account: 178 | 179 | ```bash 180 | firebase login 181 | ``` 182 | 183 | This will give you a `firebase` command, which lets you deploy your app to Firebase hosting, deploy Firebase functions, and run Firebase functions locally. 184 | 185 | 186 | ### 6. Create your `.firebaserc` file 187 | 188 | In order to know which Firebase project to use, the `firebase` command looks for the current project's ID in a file could `.firebaserc` -- which you'll need to create. The easiest way to do this is by running `firebase use --add`, selecting a project, and then naming it `default` when prompted: 189 | 190 | ``` 191 | $ firebase use --add 192 | 193 | ? Which project do you want to add? vouchchat 194 | ? What alias do you want to use for this project? (e.g. staging) default 195 | 196 | Created alias default for vouchchat. 197 | Now using alias default (vouchchat) 198 | ``` 199 | 200 | 201 | ### 7. Start the local Firebase functions server 202 | 203 | Once you've set up the Firebase CLI tools, you can start a local server in a separate terminal window to simulate your Firebase functions: 204 | 205 | ```bash 206 | npm run start:functions 207 | ``` 208 | 209 | If this has worked correctly, then you should now be able to submit the form on your landing page! 210 | 211 | You'll usually want both the React and Firebase dev servers running simultaneously, so this repository also includes a special script that will start both of them in the same tab: 212 | 213 | ```bash 214 | npm start 215 | ``` 216 | 217 | 218 | ### 8. Deploy! 219 | 220 | Once you've completed all of the earlier steps, deploying your app to the internets is simple: 221 | 222 | ```bash 223 | npm run build:development # or npm run build:production 224 | firebase deploy 225 | ``` 226 | 227 | This will build your app's distributable files with the `react-scripts` command, before calling `firebase deploy` to upload the new app to Firebase hosting, deploy cloud functions, and lock down your database's access rules. Once complete, your new app's URL will be printed to the console. 228 | 229 | If everything is set up correctly, you should now be able to submit the form in the live app with another email address, and see it appear in your Firebase console after a refresh. Congratulations -- you've got your landing page working! All that's left is to play with the styles to make it work for your brand. 230 | 231 | 232 | ### 9. Bonus step: add a domain 233 | 234 | Within the [Firebase Console](https://console.firebase.google.com)'s *Hosting* area, you can add a domain name for your landing page -- so that it looks a little more official. 235 | 236 | 237 | How does this all work though? 238 | ------------------------------ 239 | 240 | Want to build real apps that make real bacon? Then check out [React, Firebase & Bacon](https://frontarm.com/bacon) -- and while you're there, pick up a free CLI cheatsheet with many of the commands from this guide. I'll see you there! 241 | 242 | *- James K Nelson* 243 | 244 | 245 | License 246 | ------- 247 | 248 | Code is licensed under the MIT license. Images are not licensed. -------------------------------------------------------------------------------- /docs/firebase-service-key.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/docs/firebase-service-key.gif -------------------------------------------------------------------------------- /docs/firebase-setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/docs/firebase-setup.gif -------------------------------------------------------------------------------- /docs/vouch-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/docs/vouch-demo.gif -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | }, 16 | "firestore": { 17 | "rules": "firestore.rules" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | // Keep everything locked down by default 4 | match /{document=**} { 5 | allow read, write: if false; 6 | } 7 | 8 | match /counts/responses { 9 | allow get; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/actions/getResponseCount.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | 3 | const db = admin.firestore() 4 | const counts = db.collection('counts') 5 | 6 | module.exports = async () => { 7 | let ref = counts.doc('responses') 8 | let snapshot = await ref.get() 9 | 10 | return { 11 | status: 'success', 12 | data: snapshot.data(), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /functions/actions/postResponse.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | 3 | const db = admin.firestore() 4 | const increment = admin.firestore.FieldValue.increment(1) 5 | const counts = db.collection('counts') 6 | const responses = db.collection('responses') 7 | 8 | function validate({ email, name }) { 9 | if (!name) { 10 | return { 11 | name: 'required', 12 | } 13 | } 14 | if (!email) { 15 | return { 16 | email: 'required', 17 | } 18 | } 19 | if (!/.+@.+/.test(email)) { 20 | return { 21 | email: 'invalid', 22 | } 23 | } 24 | } 25 | 26 | module.exports = async ({ name, email }) => { 27 | let validationErrors = validate({ name, email }) 28 | if (validationErrors) { 29 | return { 30 | status: 'error', 31 | code: 400, 32 | issues: validationErrors, 33 | } 34 | } 35 | 36 | let error = await db.runTransaction(async transaction => { 37 | let query = responses.where('email', '==', email) 38 | let result = await transaction.get(query) 39 | if (result.size) { 40 | return { 41 | status: 'error', 42 | code: 409, 43 | issues: { 44 | email: 'not-unique', 45 | }, 46 | } 47 | } 48 | 49 | let responseRef = responses.doc() 50 | await transaction.create(responseRef, { 51 | email, 52 | name, 53 | }) 54 | 55 | let countRef = counts.doc('responses') 56 | await transaction.set(countRef, { count: increment }, { merge: true }) 57 | }) 58 | 59 | return ( 60 | error || { 61 | status: 'success', 62 | } 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const cors = require('cors') 2 | const express = require('express') 3 | const admin = require('firebase-admin') 4 | const functions = require('firebase-functions') 5 | 6 | admin.initializeApp({ 7 | credential: admin.credential.cert(require('./.serviceaccount.json')), 8 | }) 9 | 10 | /** 11 | * Remotely callable versions of actions. You'll want to use these over 12 | * a JSON API where possible. 13 | */ 14 | 15 | exports.actions = { 16 | postResponse: functions.https.onCall(require('./actions/postResponse')), 17 | } 18 | 19 | /** 20 | * Create a JSON API version of the actions, just because we can. 21 | */ 22 | 23 | const app = express() 24 | 25 | // Automatically allow cross-origin requests 26 | app.use(cors({ origin: true })) 27 | 28 | // Allow json data to be passed in via the body 29 | app.use(express.json()) 30 | 31 | const asyncMiddleware = fn => (req, res, next) => { 32 | let params = { 33 | ...req.query, 34 | ...req.params, 35 | ...req.body, 36 | } 37 | Promise.resolve(fn(params, req, res, next)) 38 | .then(result => { 39 | if (result.code) { 40 | res.status(result.code) 41 | } else if (result.status !== 'success') { 42 | res.status(500) 43 | } 44 | res.json(result) 45 | }) 46 | .catch(() => { 47 | res.status(500) 48 | }) 49 | } 50 | 51 | // build multiple CRUD interfaces: 52 | app.post('/response', asyncMiddleware(require('./actions/postResponse'))) 53 | app.get( 54 | '/response-count', 55 | asyncMiddleware(require('./actions/getResponseCount')), 56 | ) 57 | 58 | // Expose Express API as a single Cloud Function: 59 | exports.api = functions.https.onRequest(app) 60 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "express": "^4.17.1", 17 | "firebase-admin": "^8.6.0", 18 | "firebase-functions": "^3.2.0" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2016", 5 | "jsx": "preserve", 6 | "checkJs": false, 7 | "baseUrl": "./src", 8 | "strict": false 9 | }, 10 | "exclude": ["node_modules", "**/node_modules/*"] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "firebase": "^7.1.0", 7 | "history": "^4.10.1", 8 | "react": "^16.10.2", 9 | "react-dom": "^16.10.2", 10 | "react-scripts": "3.2.0", 11 | "styled-components": "^4.4.0" 12 | }, 13 | "devDependencies": { 14 | "env-cmd": "^10.0.1", 15 | "npm-run-all": "4.1.5" 16 | }, 17 | "scripts": { 18 | "start:functions": "firebase serve --only functions:api", 19 | "start:client": "react-scripts start", 20 | "start": "npm-run-all --parallel start:*", 21 | "build:production": "react-scripts build", 22 | "build:development": "env-cmd -f .env.development.local react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #102030 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 14 | 20 | 26 | 27 | 32 | 33 | 34 | 43 | Vouch 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/mstile-310x310.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frontarm/react-firebase-bacon/e89b080083f9daf4d16688f2ddbb80c8bbb3a642/public/mstile-70x70.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | import React, { useEffect, useState } from 'react' 3 | 4 | import NotFound from 'routes/404' 5 | import Landing from 'routes/landing' 6 | import Privacy from 'routes/privacy' 7 | import Thanks from 'routes/thanks' 8 | import normalizePathname from 'utils/normalizePathname' 9 | 10 | const history = createBrowserHistory() 11 | const navigate = path => history.push(path) 12 | 13 | function getRoute(location) { 14 | switch (normalizePathname(location.pathname)) { 15 | case '/': 16 | return 17 | 18 | case '/privacy': 19 | return 20 | 21 | case '/thanks': 22 | return 23 | 24 | default: 25 | return 26 | } 27 | } 28 | 29 | export default function App() { 30 | const [location, setLocation] = useState(history.location) 31 | 32 | useEffect(() => history.listen(setLocation), []) 33 | 34 | return getRoute(location) 35 | } 36 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /src/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components/macro' 2 | 3 | import { colors, dimensions } from 'theme' 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | * { 7 | appearance: none; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | background-color: ${colors.background.canvas}; 13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 14 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 15 | sans-serif; 16 | font-size: ${dimensions.oneRemInPixels}px; 17 | height: 100%; 18 | line-height: 1.5rem; 19 | min-height: 100%; 20 | } 21 | 22 | body, #root { 23 | height: 100%; 24 | margin: 0; 25 | min-height: 100%; 26 | } 27 | ` 28 | 29 | export default GlobalStyle 30 | -------------------------------------------------------------------------------- /src/backend.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app' 2 | import 'firebase/firestore' 3 | import 'firebase/functions' 4 | import config from './config' 5 | 6 | let firebaseApp = firebase.initializeApp(config.firebase) 7 | 8 | export const db = firebaseApp.firestore() 9 | export const functions = firebaseApp.functions() 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | functions.useFunctionsEmulator( 13 | process.env.REACT_APP_FUNCTIONS_URL || 'http://localhost:5000', 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/buttons.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components/macro' 2 | 3 | import { beaconRing, colors, shadows } from 'theme' 4 | 5 | export const StyledButton = styled.button` 6 | border-radius: 9999px; 7 | background-color: ${colors.primary.default}; 8 | border: none; 9 | box-shadow: ${shadows.bevel()}, ${shadows.drop()}; 10 | color: ${colors.text.reverse}; 11 | cursor: pointer; 12 | display: flex; 13 | font-size: 1rem; 14 | justify-content: center; 15 | line-height: 1rem; 16 | margin: 1rem 0; 17 | outline: none; 18 | padding: 0.5rem; 19 | position: relative; 20 | transition: opacity 200ms ease-out; 21 | width: 100%; 22 | 23 | ${beaconRing('::after', '9999px')} 24 | 25 | ${props => 26 | props.disabled && 27 | css` 28 | opacity: 0.5; 29 | `} 30 | ` 31 | -------------------------------------------------------------------------------- /src/components/cards.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | 3 | import { colors, radii, shadows } from 'theme' 4 | 5 | export const StyledCard = styled.div` 6 | background-color: ${colors.background.card}; 7 | border: 1px solid ${colors.border.default}; 8 | border-radius: ${radii.medium}; 9 | box-shadow: ${shadows.card()}; 10 | margin: 0 auto; 11 | max-width: 380px; 12 | position: relative; 13 | overflow: hidden; 14 | z-index: 0; 15 | ` 16 | -------------------------------------------------------------------------------- /src/components/fields.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/macro' 3 | 4 | import { Input } from 'components/inputs' 5 | import { colors } from 'theme' 6 | 7 | const StyledFieldLabel = styled.label` 8 | color: ${colors.text.label}; 9 | display: block; 10 | font-size: 0.8rem; 11 | font-weight: 600; 12 | margin-top: 0.5rem; 13 | ` 14 | 15 | const StyledFieldMessage = styled.div` 16 | color: ${colors.text.error}; 17 | font-size: 0.85rem; 18 | line-height: 1.4rem; 19 | margin: 0.25rem 0 1rem; 20 | text-align: left; 21 | ` 22 | 23 | export const Field = ({ label, message, onChange, ...inputProps }) => ( 24 | 25 | {label} 26 | onChange(event.target.value)} /> 27 | {message && {message}} 28 | 29 | ) 30 | -------------------------------------------------------------------------------- /src/components/inputs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/macro' 3 | 4 | import { beaconRing, colors, radii, shadows } from 'theme' 5 | 6 | const StyledInputOutline = styled.div`` 7 | const StyledInputWrapper = styled.div` 8 | position: relative; 9 | ` 10 | const StyledInput = styled.input` 11 | border: 1px solid ${colors.border.field}; 12 | border-radius: ${radii.small}; 13 | box-shadow: ${shadows.sunk()}; 14 | display: block; 15 | font-size: 1rem; 16 | padding: 0.5rem; 17 | outline: none; 18 | width: 100%; 19 | 20 | ${beaconRing(` + ${StyledInputOutline}`, radii.small)} 21 | ` 22 | 23 | export const Input = props => ( 24 | 25 | 26 | 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/links.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components/macro' 3 | 4 | import { beaconRing, colors } from 'theme' 5 | 6 | export const Link = ({ children, href, navigate, ...rest }) => { 7 | const handleClick = event => { 8 | // Let the browser handle the event directly if: 9 | // - The user used the middle/right mouse button (to open a new tab/window) 10 | // - The user was holding a modifier key 11 | // - The href is fully qualified, or a mailto link 12 | if ( 13 | event.button !== 0 || 14 | event.altKey || 15 | event.ctrlKey || 16 | event.metaKey || 17 | event.shiftKey || 18 | /^[a-z]+:/i.test(href) || 19 | href.substr(0, 2) === '//' 20 | ) { 21 | return 22 | } 23 | 24 | // Stop the browser from loading the linked page 25 | event.preventDefault() 26 | 27 | // Then use the supplied `navigate` function to handle navigation 28 | navigate(href) 29 | } 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ) 36 | } 37 | 38 | export const StyledLink = styled(Link)` 39 | color: ${props => props.color}; 40 | text-decoration: underline; 41 | position: relative; 42 | 43 | ${beaconRing('::after', '9999px')} 44 | ` 45 | 46 | StyledLink.defaultProps = { 47 | color: colors.primary.default, 48 | } 49 | -------------------------------------------------------------------------------- /src/components/loadingIndicators.js: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from 'styled-components/macro' 2 | 3 | import { colors } from 'theme' 4 | 5 | const loadingBarKeyframes = keyframes` 6 | 0% { 7 | transform: scaleX(0); 8 | } 9 | 10% { 10 | transform: scaleX(0.3); 11 | } 12 | 50% { 13 | transform: scaleX(0.7); 14 | } 15 | 90% { 16 | transform: scaleX(0.8); 17 | } 18 | 100% { 19 | transform: scaleX(1); 20 | } 21 | ` 22 | 23 | export const StyledLoadingBar = styled.div` 24 | height: 2px; 25 | width: 100%; 26 | background-color: ${props => props.color || colors.primary.default}; 27 | background-size: 35px 35px; 28 | z-index: 9999; 29 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset; 30 | transition: transform ease-in 300ms, opacity ease-in 300ms; 31 | transition-delay: 0; 32 | transform-origin: left center; 33 | transform: scaleX(0); 34 | opacity: 0; 35 | bottom: 0; 36 | 37 | ${props => 38 | props.active && 39 | css` 40 | animation: ${loadingBarKeyframes} 10s ease-out; 41 | animation-fill-mode: forwards; 42 | opacity: 1; 43 | 44 | /** 45 | * Wait 100ms before showing any loading bar. This should be long enough 46 | * prevent the display of a loading bar for instant page loads, while 47 | * short enough to help the user know that something is happening on 48 | * pages with async data. 49 | */ 50 | transition-delay: 100ms; 51 | `} 52 | ` 53 | 54 | // --- 55 | 56 | const spinnerRotatorAnimation = keyframes` 57 | 0% { 58 | transform: rotateZ(0deg); 59 | } 60 | 100% { 61 | transform: rotateZ(360deg) 62 | } 63 | ` 64 | 65 | export const StyledSpinner = styled.div` 66 | position: ${props => props.position}; 67 | background-color: transparent; 68 | border-radius: 50%; 69 | border: 2px solid ${props => props.color}; 70 | border-left-color: transparent; 71 | display: inline-block; 72 | width: ${props => props.size}; 73 | height: ${props => props.size}; 74 | animation: ${spinnerRotatorAnimation} 1.8s linear infinite; 75 | ` 76 | 77 | StyledSpinner.defaultProps = { 78 | position: 'relative', 79 | color: colors.primary.light, 80 | size: '100%', 81 | } 82 | -------------------------------------------------------------------------------- /src/components/narrowCardLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'styled-components/macro' 3 | 4 | import { StyledCard } from 'components/cards' 5 | import { Link, StyledLink } from 'components/links' 6 | import logo from 'media/logo.svg' 7 | import vouch from 'media/vouch.svg' 8 | import { colors, dimensions, media } from 'theme' 9 | 10 | export default function NarrowCardLayout({ children, navigate }) { 11 | return ( 12 |
22 | 30 | 37 | Logo 48 | Vouch 55 | 56 | {children} 57 | 58 |
65 | 66 | Privacy Policy 67 | 68 | 72 | · 73 | 74 | 78 | See source at GitHub 79 | 80 |
81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/components/typography.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro' 2 | 3 | import { colors, media } from 'theme' 4 | 5 | export const StyledHaiku = styled.p` 6 | color: ${colors.text.alt}; 7 | font-size: 1rem; 8 | line-height: 1.5rem; 9 | margin: 1.5rem 0; 10 | text-align: center; 11 | 12 | ${media.smallPhoneOnly` 13 | font-size: 0.9rem; 14 | line-height: 1.4rem; 15 | `} 16 | ` 17 | 18 | export const StyledIssue = styled.p` 19 | color: ${colors.text.error}; 20 | font-size: 0.75rem; 21 | margin-top: 0.75rem; 22 | text-align: center; 23 | ` 24 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | firebase: { 3 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 4 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 5 | databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL, 6 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 7 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 8 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 9 | appId: process.env.REACT_APP_FIREBASE_APP_ID, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import App from 'App' 5 | import GlobalStyle from 'GlobalStyle' 6 | 7 | ReactDOM.render( 8 | <> 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ) 14 | -------------------------------------------------------------------------------- /src/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /src/media/vouch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/routes/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NarrowCardLayout from 'components/narrowCardLayout' 3 | import { StyledHaiku } from 'components/typography' 4 | 5 | export default function NotFound({ navigate }) { 6 | return ( 7 | 8 | 9 | I couldn't find it 10 |
11 | The page probably hates me 12 |
13 | I'm really depressed 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/landing.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { css } from 'styled-components/macro' 3 | 4 | import { db, functions } from 'backend' 5 | import { StyledButton } from 'components/buttons' 6 | import NarrowCardLayout from 'components/narrowCardLayout' 7 | import { Field } from 'components/fields' 8 | import { StyledLoadingBar, StyledSpinner } from 'components/loadingIndicators' 9 | import { StyledHaiku, StyledIssue } from 'components/typography' 10 | import { issuesIntersection } from 'utils/issues' 11 | 12 | const postResponse = functions.httpsCallable('actions-postResponse') 13 | 14 | const messages = { 15 | base: { 16 | error: 'Something went wrong', 17 | }, 18 | email: { 19 | invalid: "That email address doesn't look quite right.", 20 | 'not-unique': 'This email has already been used.', 21 | required: "You'll need an email to join the list.", 22 | }, 23 | name: { 24 | required: 'Who are you, though?', 25 | }, 26 | } 27 | 28 | function validate({ email, name }) { 29 | let issues = {} 30 | if (!name) { 31 | issues.name = 'required' 32 | } 33 | if (!email) { 34 | issues.email = 'required' 35 | } else if (!/.+@.+/.test(email)) { 36 | issues.email = 'invalid' 37 | } 38 | return Object.keys(issues).length ? issues : undefined 39 | } 40 | 41 | export default function Landing({ navigate }) { 42 | const [responseCount, setResponseCount] = useState(undefined) 43 | const [name, setName] = useState('') 44 | const [email, setEmail] = useState('') 45 | const [status, setStatus] = useState({ 46 | type: 'fresh', 47 | }) 48 | const params = { name, email } 49 | 50 | useEffect(() => { 51 | const ref = db.collection('counts').doc('responses') 52 | 53 | // There's no need to continue listening for the number of subscribers 54 | // after the user has already subscribed. 55 | return ref.onSnapshot(snapshot => { 56 | setResponseCount((snapshot.exists && snapshot.data().count) || 0) 57 | }) 58 | }, []) 59 | 60 | const handleSubmit = async event => { 61 | event.preventDefault() 62 | 63 | const issues = validate(params) 64 | if (issues) { 65 | setStatus({ 66 | type: 'error', 67 | issues, 68 | }) 69 | return 70 | } 71 | 72 | setStatus({ 73 | type: 'pending', 74 | }) 75 | 76 | try { 77 | const { data } = await postResponse(params) 78 | if (data.status === 'error') { 79 | setStatus({ 80 | type: 'error', 81 | issues: data.issues || { 82 | base: 'error', 83 | }, 84 | params, 85 | }) 86 | } else { 87 | navigate('/thanks') 88 | } 89 | } catch (error) { 90 | console.error(error) 91 | setStatus({ 92 | type: 'error', 93 | issues: { 94 | base: 'error', 95 | }, 96 | }) 97 | } 98 | } 99 | 100 | const canSubmit = responseCount !== undefined && status.type !== 'pending' 101 | const submitIssues = status.issues || {} 102 | const validationIssues = validate(params) 103 | const unresolvedIssues = 104 | issuesIntersection(submitIssues, validationIssues) || {} 105 | 106 | // As this issue depends on server state, it won't be picked up by validate, 107 | // and thus it won't be in the intersection either. 108 | if ( 109 | status.issues && 110 | status.issues.email === 'not-unique' && 111 | status.params.email === email 112 | ) { 113 | unresolvedIssues.email = 'not-unique' 114 | } 115 | 116 | return ( 117 | 118 |
119 | 120 | A social network,
121 | Where you are the customer.
122 | Ad free. Launching soon. 123 |
124 | 130 | 137 | {submitIssues.base && ( 138 | {messages.base[submitIssues.base]} 139 | )} 140 | 141 | {responseCount === undefined ? ( 142 | 143 | ) : responseCount > 1 ? ( 144 | `Vouch with ${responseCount} others` 145 | ) : ( 146 | `I'll vouch for that` 147 | )} 148 | 149 | 150 | 159 |
160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /src/routes/privacy.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NarrowCardLayout from 'components/narrowCardLayout' 3 | import { StyledHaiku } from 'components/typography' 4 | 5 | export default function Privacy({ navigate }) { 6 | return ( 7 | 8 | 9 | Your privacy is 10 |
11 | Very important to us 12 |
I wrote a poem 13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/thanks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NarrowCardLayout from 'components/narrowCardLayout' 3 | import { StyledHaiku } from 'components/typography' 4 | 5 | export default function Privacy({ navigate }) { 6 | return ( 7 | 8 | 9 | Thanks for joining in! 10 |
11 | When we're ready to wow you, 12 |
13 | You'll get an email. 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components/macro' 2 | 3 | export const breakpoints = { 4 | mediumPhonePlus: '360px', 5 | tabletPlus: '768px', 6 | } 7 | 8 | export const colors = { 9 | background: { 10 | canvas: '#f5f7fa', 11 | card: '#ffffff', 12 | }, 13 | beacon: { 14 | focus: 'rgba(68, 136, 221, 0.75)', 15 | focusGlow: 'rgba(68, 136, 221, 0.4)', 16 | }, 17 | border: { 18 | default: '#eaecf2', 19 | field: '#e0e8ec', 20 | }, 21 | primary: { 22 | default: '#102030', 23 | light: '#aabbcc', 24 | }, 25 | text: { 26 | alt: '#607080', 27 | default: '#282A2C', 28 | error: '#733939', 29 | label: '#334455', 30 | reverse: 'rgba(255, 255, 255, 0.93)', 31 | }, 32 | } 33 | 34 | export const dimensions = { 35 | narrowClampWidth: '380px', 36 | oneRemInPixels: 16, 37 | } 38 | 39 | const mediaFactory = query => (...args) => css` 40 | @media screen and ${query} { 41 | ${css.apply(null, args)} 42 | } 43 | ` 44 | 45 | export const mediaQueries = { 46 | smallPhoneOnly: `(max-width: calc(${breakpoints.mediumPhonePlus} - 1px))`, 47 | phoneOnly: `(max-width: calc(${breakpoints.tabletPlus} - 1px))`, 48 | mediumPhonePlus: `(min-width: ${breakpoints.mediumPhonePlus})`, 49 | tabletPlus: `(min-width: ${breakpoints.tabletPlus})`, 50 | } 51 | export const media = { 52 | smallPhoneOnly: mediaFactory(mediaQueries.smallPhoneOnly), 53 | phoneOnly: mediaFactory(mediaQueries.phoneOnly), 54 | mediumPhonePlus: mediaFactory(mediaQueries.mediumPhonePlus), 55 | tabletPlus: mediaFactory(mediaQueries.tabletPlus), 56 | } 57 | 58 | export const radii = { 59 | small: '4px', 60 | medium: '8px', 61 | } 62 | 63 | export const shadows = { 64 | beacon: color => ` 65 | 0 0 0 2px ${colors.beacon[color]}, 66 | 0 0 4px 3px ${colors.beacon[color + 'Glow']} 67 | `, 68 | bevel: () => ` 69 | 1px 1px 1px rgba(255, 255, 255, 0.12) inset, 70 | -1px -1px 1px rgba(0, 0, 0, 0.08) inset 71 | `, 72 | card: () => ` 73 | 0 0 5px 3px rgba(0, 0, 0, 0.01), 74 | 0 0 2px 0px rgba(0, 0, 0, 0.02) 75 | `, 76 | drop: () => ` 77 | 1px 1px 1px rgba(255, 255, 255, 0.12) inset, 78 | -1px -1px 1px rgba(0, 0, 0, 0.08) inset 79 | `, 80 | sunk: () => ` 81 | 2px 2px 2px rgba(16, 32, 48, 0.03) inset 82 | `, 83 | } 84 | 85 | export const beaconRing = (selector, radius) => 86 | css` 87 | ${selector} { 88 | content: ' '; 89 | position: absolute; 90 | border-radius: ${radius}; 91 | left: 0px; 92 | right: 0px; 93 | top: 0px; 94 | bottom: 0px; 95 | z-index: -1; 96 | } 97 | :focus${selector} { 98 | box-shadow: ${shadows.beacon('focus')}; 99 | } 100 | ` 101 | -------------------------------------------------------------------------------- /src/utils/issues.js: -------------------------------------------------------------------------------- 1 | export function issuesIntersection(x, y) { 2 | if (!y || !x) { 3 | return 4 | } 5 | let keys = Object.keys(y) 6 | let intersection = {} 7 | for (let key of keys) { 8 | if (x[key] === y[key]) { 9 | intersection[key] = x[key] 10 | } 11 | } 12 | return intersection 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/normalizePathname.js: -------------------------------------------------------------------------------- 1 | export default function normalizePathname(pathname) { 2 | if (pathname === '/' || pathname === '') { 3 | return '/' 4 | } 5 | 6 | // Add leading slash 7 | pathname = pathname[0] !== '/' ? '/' + pathname : pathname 8 | 9 | // Strip trailing slash 10 | pathname = 11 | pathname[pathname.length - 1] === '/' ? pathname.slice(0, -1) : pathname 12 | 13 | return pathname 14 | } 15 | --------------------------------------------------------------------------------