├── .firebase ├── hosting.YnVpbGQ.cache └── hosting.cHVibGlj.cache ├── .gitignore ├── LICENSE ├── README.md ├── env_template.txt ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .eslintrc.json ├── .gitignore ├── firebase-debug 21.log ├── index.js └── package.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── firebase-messaging-sw.js ├── index.html └── manifest.json ├── src ├── App.js ├── App.test.js ├── MainRouter.js ├── components │ ├── BodyWrapper.js │ ├── Button.js │ ├── CenteredDiv.js │ ├── Close.js │ ├── Form.js │ ├── H1.js │ ├── H2.js │ ├── Hamburger.js │ ├── Icon.js │ ├── Input.js │ ├── Message.js │ ├── MobileMenuBar.js │ ├── Overlay.js │ ├── P.js │ ├── SocialAuthButton.js │ ├── Spinner.js │ ├── Switch.js │ ├── Toast.js │ └── index.js ├── containers │ ├── Confirmed.js │ ├── Dashboard.js │ ├── Header.js │ ├── Lander.js │ ├── MenuOverlay.js │ ├── PrivacyPolicy.js │ ├── Profile.js │ └── SignIn.js ├── contexts │ ├── toastContext.js │ └── userContext.js ├── firebase.js ├── helpers │ └── cloudFunctions.js ├── index.css ├── index.js ├── serviceWorker.js └── themes │ ├── GlobalStyle.js │ ├── colors.js │ ├── icons.js │ ├── icons │ ├── btn_google_signin_light_normal_web@2x.png │ ├── flogo-HexRBG-Wht-72.svg │ ├── github.svg │ ├── icons8-google-48.png │ └── icons8-google-96.png │ ├── index.js │ └── metrics.js └── storage.rules /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1574796918192,9c10ab6937a0901c29578a72c2a3d107f8a5511e1b2e96001f508cdd07d54a5f 2 | firebase-messaging-sw.js,1574796908306,62632d06385204473cd8204c4cc57c99d2abe26b7491af02d2c0286383394e54 3 | favicon.ico,1574796908305,b72f7455f00e4e58792d2bca892abb068e2213838c0316d6b7a0d6d16acd1955 4 | index.html,1574796918192,5230e7bb9a8bf77f64d715895e3a24570f52740ff8b365097dda49aaefde5f2f 5 | manifest.json,1574796908307,655e75faa969091d830053a47ac2395c6fc8c1338913049a85c52998bb3972c8 6 | precache-manifest.13d62681de50cf679959f87215ae16b5.js,1574796918192,b799100d399d25d20be9c937b93449c03033d93334207c3f4e5ba1e92b580a8c 7 | static/css/main.c07093cd.chunk.css,1574796918194,0de57ff8128e17d3c9d51fe23a318564b276981c91143b1010338a71ba698932 8 | service-worker.js,1574796918192,02bcd0511a4a5a1f302d9242f775fbe4edcd6a20cf422cc6d4c2325b186d02c7 9 | static/css/main.c07093cd.chunk.css.map,1574796918206,5378a72d798d68178cc279d16a03e64817964d53a395ccd4abf50313042810e3 10 | static/js/runtime~main.229c360f.js,1574796918206,dbe189fe130313d04dc42edcaa021db9317ce6d58c07ab66fe538fdfe41fe6d7 11 | static/media/flogo-HexRBG-Wht-72.9700c594.svg,1574796918193,0e0769b80fd9c815abe746cae21dce0338df51c1ab8e5db129761e0b33e608f2 12 | static/js/runtime~main.229c360f.js.map,1574796918207,b2f1f5578e572791ed8967e3d0090b7eb2ec5f9d87d1bd433d4d7ffdb5d15f5e 13 | static/js/main.edee57b8.chunk.js,1574796918205,bd5ba75014d9b3a03e742e802b3604a4cef224ced3062e62a3fe5b3ed50d8cd7 14 | static/media/github.5cff78e4.svg,1574796918205,580d7a1f16609f34a26eca1679a6ec5a61b5d11657e66f99bcbf0d0517c25610 15 | static/js/main.edee57b8.chunk.js.map,1574796918206,aa13326b14a4fa45e776edb8eefd0f24eb36a31f66da47650430c48819f3a10f 16 | static/js/1.47d48538.chunk.js,1574796918205,ad8e8220b5901c0e686d350f6303c47733ce2e0ae10bcb5a91c6899c46f4fe1e 17 | static/js/1.47d48538.chunk.js.map,1574796918206,b296604c319188de88f53835e4446dbcbb313d370943bfd8ced56f351d8b93da 18 | -------------------------------------------------------------------------------- /.firebase/hosting.cHVibGlj.cache: -------------------------------------------------------------------------------- 1 | firebase-messaging-sw.js,1574710129214,62632d06385204473cd8204c4cc57c99d2abe26b7491af02d2c0286383394e54 2 | favicon.ico,1574710129213,b72f7455f00e4e58792d2bca892abb068e2213838c0316d6b7a0d6d16acd1955 3 | index.html,1574710129214,de2ac4fdf014d6b182c11283d301d2163e4786a047d9cad8395a0e90b0181f0a 4 | manifest.json,1574710129214,655e75faa969091d830053a47ac2395c6fc8c1338913049a85c52998bb3972c8 5 | -------------------------------------------------------------------------------- /.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 | /.vscode 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | #firebase env 28 | .firebaserc 29 | /functions/.env.yaml 30 | /functions/node_modules 31 | /functions/package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jeremy Barbe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A fully-featured single-page app template, hosted and running in minutes. Based on Create-React-App and Firebase. Written with React-Hooks. 2 | 3 | **Live demo at: (https://react-firebase-essentials.com)** 4 | 5 | This is a full application template intended to provide the basic features that most online products or services need to get started, so that you can get straight into building out the important parts of your MVP without getting bogged down with the boring tasks that you've already done for a million apps before. 6 | 7 | **Features:** 8 | 9 | - Modern authentication UI flows have already been built out, it's as simple as plugging in your Firebase API keys. Email, Facebook and Google are all included methods. 10 | - Mobile-ready responsive design. 11 | - Utilizes Styled-Components, carefully using global variables that allow you to quickly and easily adjust to your tastes. 12 | - It's built entirely with React Hooks and the new Context API. Built on top of Create-React-App. 13 | - Requires very few dependencies. The least popular module it uses has 6.4k stars on Github. 14 | - Basic security rules have already been written for the Firestore DB. 15 | - Push Notifications set up out of the box, with a cloud function supplied for triggering messages. 16 | - Dark Mode! You gotta have dark mode! 17 | 18 | **Getting set up:** (in minutes) 19 | 20 | 1. Make sure you have the Firebase CLI installed. Create a new project in the Firebase console and **make sure** you go into project settings and choose a GCP resource location under the general tab. 21 | 2. Enable Facebook, Google, and Email/Password authentication in the Firebase console. When enabling "Email/Password", be sure to enable "Email link" as well. (OAuth with Facebook and Google will require some additional steps, outlined when you view each method in the Firebase authentication console) 22 | 3. Clone the project and `npm run setup`. 23 | 4. Select your Firebase project in the command line by using `firebase use [YOUR_PROJECT_ID]`. 24 | 5. Add your Firebase keys to `.env.development.local` and `.env.production.local`. 25 | 6. Go into `public/firebase-messaging-sw.js` and manually change the messagingSenderId, which will be the same as `REACT_APP_FIREBASE_MESSAGING_ID` in your `.env` files. 26 | 7. Run `npm run build` to create a production build of your project, and `firebase deploy`. Your application should now be hosted and ready to visit. 27 | -------------------------------------------------------------------------------- /env_template.txt: -------------------------------------------------------------------------------- 1 | # Leave this the same for your development file, 2 | # and change it to whichever URL you want users 3 | # to access your site with in the production file 4 | REACT_APP_BASE_URL=localhost:3000 5 | 6 | # Found in project settings under the General tab 7 | REACT_APP_FIREBASE_PROJECT_ID= 8 | REACT_APP_FIREBASE_API_KEY= 9 | 10 | # Found in project settings under the Cloud Messaging tab 11 | REACT_APP_FIREBASE_MESSAGING_CERT= 12 | REACT_APP_FIREBASE_MESSAGING_ID= 13 | 14 | # Found in the Hosting tab 15 | REACT_APP_FIREBASE_DOMAIN= -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "hosting": { 7 | "public": "build", 8 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | }, 16 | "storage": { 17 | "rules": "storage.rules" 18 | }, 19 | "functions": { 20 | "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run lint"], 21 | "source": "functions" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write: if false; 5 | } 6 | match /users/{uid} { 7 | allow write: if request.resource.data.keys().hasOnly(['email']) || 8 | request.resource.data.firstName is string 9 | && request.resource.data.lastName is string 10 | && request.auth.uid == uid || 11 | request.resource.data.pushTokenWeb is string 12 | && request.auth.uid == uid; 13 | allow update: if request.resource.data.firstName is string 14 | && request.resource.data.lastName is string 15 | && request.auth.uid == uid || 16 | request.resource.data.pushTokenWeb is string 17 | && request.auth.uid == uid; 18 | allow read: if request.auth.uid == uid; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /functions/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 6 5 | }, 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | // Removed rule "disallow the use of console" from recommended eslint rules 12 | "no-console": "off", 13 | 14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 15 | "no-regex-spaces": "off", 16 | 17 | // Removed rule "disallow the use of debugger" from recommended eslint rules 18 | "no-debugger": "off", 19 | 20 | // Removed rule "disallow unused variables" from recommended eslint rules 21 | "no-unused-vars": "off", 22 | 23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules 24 | "no-mixed-spaces-and-tabs": "off", 25 | 26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules 27 | "no-undef": "off", 28 | 29 | // Warn against template literal placeholder syntax in regular strings 30 | "no-template-curly-in-string": 1, 31 | 32 | // Warn if return statements do not either always or never specify values 33 | "consistent-return": 1, 34 | 35 | // Warn if no return statements in callbacks of array methods 36 | "array-callback-return": 1, 37 | 38 | // Require the use of === and !== 39 | "eqeqeq": 2, 40 | 41 | // Disallow the use of alert, confirm, and prompt 42 | "no-alert": 2, 43 | 44 | // Disallow the use of arguments.caller or arguments.callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons without type-checking operators 48 | "no-eq-null": 2, 49 | 50 | // Disallow the use of eval() 51 | "no-eval": 2, 52 | 53 | // Warn against extending native types 54 | "no-extend-native": 1, 55 | 56 | // Warn against unnecessary calls to .bind() 57 | "no-extra-bind": 1, 58 | 59 | // Warn against unnecessary labels 60 | "no-extra-label": 1, 61 | 62 | // Disallow leading or trailing decimal points in numeric literals 63 | "no-floating-decimal": 2, 64 | 65 | // Warn against shorthand type conversions 66 | "no-implicit-coercion": 1, 67 | 68 | // Warn against function declarations and expressions inside loop statements 69 | "no-loop-func": 1, 70 | 71 | // Disallow new operators with the Function object 72 | "no-new-func": 2, 73 | 74 | // Warn against new operators with the String, Number, and Boolean objects 75 | "no-new-wrappers": 1, 76 | 77 | // Disallow throwing literals as exceptions 78 | "no-throw-literal": 2, 79 | 80 | // Require using Error objects as Promise rejection reasons 81 | "prefer-promise-reject-errors": 2, 82 | 83 | // Enforce “for” loop update clause moving the counter in the right direction 84 | "for-direction": 2, 85 | 86 | // Enforce return statements in getters 87 | "getter-return": 2, 88 | 89 | // Disallow await inside of loops 90 | "no-await-in-loop": 2, 91 | 92 | // Disallow comparing against -0 93 | "no-compare-neg-zero": 2, 94 | 95 | // Warn against catch clause parameters from shadowing variables in the outer scope 96 | "no-catch-shadow": 1, 97 | 98 | // Disallow identifiers from shadowing restricted names 99 | "no-shadow-restricted-names": 2, 100 | 101 | // Enforce return statements in callbacks of array methods 102 | "callback-return": 2, 103 | 104 | // Require error handling in callbacks 105 | "handle-callback-err": 2, 106 | 107 | // Warn against string concatenation with __dirname and __filename 108 | "no-path-concat": 1, 109 | 110 | // Prefer using arrow functions for callbacks 111 | "prefer-arrow-callback": 1, 112 | 113 | // Return inside each then() to create readable and reusable Promise chains. 114 | // Forces developers to return console logs and http calls in promises. 115 | "promise/always-return": 2, 116 | 117 | //Enforces the use of catch() on un-returned promises 118 | "promise/catch-or-return": 2, 119 | 120 | // Warn against nested then() or catch() statements 121 | "promise/no-nesting": 1 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/firebase-debug 21.log: -------------------------------------------------------------------------------- 1 | [debug] [2019-03-19T13:55:18.899Z] ---------------------------------------------------------------------- 2 | [debug] [2019-03-19T13:55:18.901Z] Command: /Users/jeremybarbe/.nvm/versions/node/v10.0.0/bin/node /Users/jeremybarbe/.nvm/versions/node/v10.0.0/bin/firebase deploy --only functions 3 | [debug] [2019-03-19T13:55:18.902Z] CLI Version: 6.2.2 4 | [debug] [2019-03-19T13:55:18.902Z] Platform: darwin 5 | [debug] [2019-03-19T13:55:18.902Z] Node Version: v10.0.0 6 | [debug] [2019-03-19T13:55:18.910Z] Time: Tue Mar 19 2019 21:55:18 GMT+0800 (CST) 7 | [debug] [2019-03-19T13:55:18.910Z] ---------------------------------------------------------------------- 8 | [debug] 9 | [debug] [2019-03-19T13:55:18.931Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 10 | [debug] [2019-03-19T13:55:18.931Z] > authorizing via signed-in user 11 | [debug] [2019-03-19T13:55:18.931Z] [iam] checking project grippping for permissions ["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get"] 12 | [debug] [2019-03-19T13:55:18.934Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/grippping:testIamPermissions 13 | 14 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const admin = require("firebase-admin"); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.sendPushNotification = functions.https.onCall((data, context) => { 6 | var message = { 7 | notification: { 8 | title: data.title, 9 | body: data.body 10 | }, 11 | token: data.token 12 | }; 13 | 14 | // Send a message to the device corresponding to the provided 15 | // registration token. 16 | admin 17 | .messaging() 18 | .send(message) 19 | .then(response => { 20 | console.log("Successfully sent message:", response); 21 | return; 22 | }) 23 | .catch(error => { 24 | console.log("Error sending message:", error); 25 | return; 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase serve --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.18.0", 14 | "firebase-admin": "^8.1.0", 15 | "firebase-functions": "^2.1.0", 16 | "request": "^2.88.0" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^4.12.0", 20 | "eslint-plugin-promise": "^3.6.0" 21 | }, 22 | "engines": { 23 | "node": "8" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-essentials", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "firebase": "^7.3.0", 7 | "react": "^16.8.1", 8 | "react-dom": "^16.8.1", 9 | "react-router-dom": "^4.3.1", 10 | "react-scripts": "2.1.1", 11 | "react-transition-group": "^2.6.0", 12 | "styled-components": "^4.1.3" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject", 19 | "setup": "npm i && cd functions && npm i && cd .. && cp env_template.txt .env.production.local && cp env_template.txt .env.development.local" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ], 30 | "devDependencies": { 31 | "eslint-plugin-react-hooks": "0.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eemebarbe/react-firebase-essentials/487d0edb0ae05238db41e0a444b9a8b01288bed4/public/favicon.ico -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts("https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js"); 2 | importScripts("https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js"); 3 | 4 | firebase.initializeApp({ 5 | messagingSenderId: "721701880005" 6 | }); 7 | 8 | const messaging = firebase.messaging(); 9 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React Firebase Essentials 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff", 15 | "gcm_sender_id": "103953800507" 16 | } 17 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ToastProvider } from "./contexts/toastContext"; 3 | import { UserProvider } from "./contexts/userContext"; 4 | import MainRouter from "./MainRouter"; 5 | 6 | const App = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /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/MainRouter.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Redirect, 6 | Switch 7 | } from "react-router-dom"; 8 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 9 | import Lander from "./containers/Lander"; 10 | import SignIn from "./containers/SignIn"; 11 | import Dashboard from "./containers/Dashboard"; 12 | import Confirmed from "./containers/Confirmed"; 13 | import Profile from "./containers/Profile"; 14 | import Header from "./containers/Header"; 15 | import PrivacyPolicy from "./containers/PrivacyPolicy"; 16 | import { UserContext } from "./contexts/userContext"; 17 | import { 18 | CenteredDiv, 19 | H1, 20 | Toast, 21 | Spinner, 22 | MobileMenuBar, 23 | BodyWrapper, 24 | P 25 | } from "./components"; 26 | import styled, { ThemeProvider } from "styled-components"; 27 | import GlobalStyle from "./themes/GlobalStyle"; 28 | import firebase from "./firebase.js"; 29 | import "firebase/firestore"; 30 | import { colors } from "./themes"; 31 | import { sendPushNotification } from "./helpers/cloudFunctions"; 32 | 33 | const AppWrapper = styled.div` 34 | height: 100%; 35 | width: 100%; 36 | overflow: hidden; 37 | position: absolute; 38 | `; 39 | 40 | const MainRouter = () => { 41 | const [initializationComplete, setInitComplete] = useState(false); 42 | const { userState, userDispatch } = useContext(UserContext); 43 | const userId = userState.userId; 44 | const db = firebase.firestore(); 45 | 46 | useEffect(() => { 47 | sendPushNotification({ 48 | token: userState.userData.pushTokenWeb, 49 | title: "Boop", 50 | body: "shoop" 51 | }); 52 | 53 | firebase.auth().onAuthStateChanged(user => { 54 | if (!!user) { 55 | const uid = firebase.auth().currentUser.uid; 56 | db.collection("users") 57 | .doc(uid) 58 | .get() 59 | .then(res => { 60 | if (res.data() && res.data().firstName) { 61 | userDispatch( 62 | { type: "updateProfile", payload: res.data() }, 63 | { type: "verifying", payload: false } 64 | ); 65 | } 66 | userDispatch({ type: "userId", payload: uid }); 67 | setInitComplete(true); 68 | }); 69 | } else { 70 | userDispatch({ 71 | type: "signOut" 72 | }); 73 | setInitComplete(true); 74 | } 75 | }); 76 | }, []); 77 | 78 | const noMatch = () => { 79 | return ( 80 | 81 |

Oops!

82 |

You're looking for a page that doesn't exist.

83 |
84 | ); 85 | }; 86 | 87 | const routeWithAuth = destination => { 88 | return !userId ? ( 89 | 94 | ) : ( 95 | destination 96 | ); 97 | }; 98 | 99 | const reRouteIfAuthenticated = destination => { 100 | return userId ? ( 101 | 106 | ) : ( 107 | destination 108 | ); 109 | }; 110 | 111 | const nestedSwitch = () => { 112 | return ( 113 | <> 114 | {userId && } 115 |
116 | ( 118 | 119 | 124 | 125 | reRouteIfAuthenticated()} 129 | /> 130 | reRouteIfAuthenticated()} 133 | /> 134 | routeWithAuth()} 137 | /> 138 | routeWithAuth()} 141 | /> 142 | } 145 | /> 146 | 147 | 148 | 149 | 150 | )} 151 | /> 152 | 153 | ); 154 | }; 155 | 156 | const router = () => { 157 | return ( 158 | 159 | <> 160 | 161 | 162 | } /> 163 | 164 | 165 | 166 | 167 | ); 168 | }; 169 | 170 | const renderApp = () => { 171 | const app = !initializationComplete ? ( 172 | 173 | 174 | 175 | ) : ( 176 | router() 177 | ); 178 | return app; 179 | }; 180 | 181 | const styleMode = window.localStorage.getItem("styleMode"); 182 | return ( 183 | 186 | <> 187 | 188 | {renderApp()} 189 | 190 | 191 | ); 192 | }; 193 | 194 | export default MainRouter; 195 | -------------------------------------------------------------------------------- /src/components/BodyWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | 5 | const BodyOuter = styled.div` 6 | display: flex; 7 | justify-content: center; 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | overflow: hidden; 12 | -webkit-overflow-scrolling: touch; 13 | overflow-y: scroll; 14 | &.fade-appear, 15 | &.fade-enter { 16 | opacity: 0; 17 | z-index: 1; 18 | transform: translateX(24px); 19 | @media (max-width: 480px) { 20 | transform: translateX(0px); 21 | } 22 | } 23 | &.fade-appear-active, 24 | &.fade-enter.fade-enter-active { 25 | opacity: 1; 26 | transform: translateX(0px); 27 | transition: opacity 400ms linear 400ms, transform 400ms ease-out 400ms; 28 | } 29 | 30 | &.fade-exit { 31 | opacity: 1; 32 | transform: translateX(0px); 33 | } 34 | &.fade-exit.fade-exit-active { 35 | opacity: 0; 36 | transform: translateX(-24px); 37 | transition: opacity 400ms linear, transform 400ms ease-in; 38 | @media (max-width: 480px) { 39 | transform: translateX(0px); 40 | } 41 | } 42 | `; 43 | 44 | const BodyWrapper = styled.div` 45 | padding: 0px ${metrics.bodyPadding}px; 46 | padding-top: ${metrics.headerHeight}px; 47 | width: ${metrics.bodyWidth}px; 48 | @media (max-width: 480px) { 49 | width: 100%; 50 | padding-top: 0px; 51 | } 52 | `; 53 | 54 | const BodyInner = styled.div` 55 | padding-bottom: ${metrics.baseUnit * 4}px; 56 | @media (max-width: 480px) { 57 | padding-bottom: ${metrics.baseUnit * 4}px; 58 | } 59 | `; 60 | 61 | const Wrapper = props => { 62 | const scrollRef = useRef(); 63 | 64 | useEffect(() => { 65 | if (scrollRef.current) { 66 | scrollRef.current.scrollTop = 0; 67 | } 68 | }, []); 69 | 70 | return ( 71 | 72 | 73 | {props.children} 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default Wrapper; 80 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | import { Spinner } from "../components"; 5 | 6 | const Button = styled.button` 7 | min-height: ${metrics.baseUnit * 4}px; 8 | width: ${metrics.baseUnit * 20}px; 9 | background-color: ${props => props.color || props.theme.primaryButton}; 10 | color: ${props => props.theme.detailText}; 11 | border: 0; 12 | padding: 0; 13 | border-radius: ${metrics.globalBorderRadius}px; 14 | margin-left: ${props => (props.marginLeft ? metrics.baseUnit + "px" : 0)}; 15 | margin-right: ${props => (props.marginRight ? metrics.baseUnit + "px" : 0)}; 16 | margin-bottom: ${props => (props.marginBottom ? metrics.baseUnit + "px" : 0)}; 17 | margin-top: ${props => (props.marginTop ? metrics.baseUnit + "px" : 0)}; 18 | font-size: ${metrics.smallText}px; 19 | cursor: pointer; 20 | outline: 0; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | font-weight: 700; 25 | @media (max-width: 480px) { 26 | width: 100%; 27 | } 28 | `; 29 | 30 | const ButtonWithLoadState = props => { 31 | const loadState = () => { 32 | if (props.loading) { 33 | return ; 34 | } else { 35 | return props.children; 36 | } 37 | }; 38 | return ( 39 | 42 | ); 43 | }; 44 | 45 | export default ButtonWithLoadState; 46 | -------------------------------------------------------------------------------- /src/components/CenteredDiv.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const CenteredDiv = styled.div` 4 | position: relative; 5 | z-index: 40; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: ${props => (props.horizontal ? "center" : "flex-start")}; 9 | align-items: ${props => (props.vertical ? "center" : "flex-start")}; 10 | height: 100%; 11 | text-align: center; 12 | @media (max-width: 480px) { 13 | text-align: left; 14 | } 15 | `; 16 | 17 | export default CenteredDiv; 18 | -------------------------------------------------------------------------------- /src/components/Close.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 4 | import { metrics } from "../themes"; 5 | 6 | const CloseStyle = styled.div` 7 | position: absolute; 8 | flex-direction: column; 9 | justify-content: center; 10 | height: ${metrics.headerHeight / 3}px; 11 | width: ${metrics.headerHeight / 3}px; 12 | &:before, 13 | &:after { 14 | position: absolute; 15 | margin-left: ${metrics.baseUnit}px; 16 | content: " "; 17 | height: ${metrics.baseUnit * 3 - 2}px; 18 | @media (max-width: 480px) { 19 | height: ${metrics.baseUnit * 2 - 2}px; 20 | } 21 | width: 1px; 22 | background-color: ${props => props.theme.mainText}; 23 | } 24 | &:before { 25 | transform: rotate(45deg); 26 | } 27 | &:after { 28 | transform: rotate(-45deg); 29 | } 30 | &.grow-appear:before, 31 | &.grow-enter:before, 32 | &.grow-appear:after, 33 | &.grow-enter:after { 34 | height: 0px; 35 | z-index: 1; 36 | } 37 | &.grow-appear-active:before, 38 | &.grow-enter.grow-enter-active:before, 39 | &.grow-appear-active:after, 40 | &.grow-enter.grow-enter-active:after { 41 | height: ${metrics.baseUnit * 3 - 2}px; 42 | transition: height 400ms ease-out; 43 | @media (max-width: 480px) { 44 | height: ${metrics.baseUnit * 2 - 2}px; 45 | } 46 | } 47 | &.grow-exit:before, 48 | &.grow-exit:after { 49 | height: ${metrics.baseUnit * 3 - 2}px; 50 | @media (max-width: 480px) { 51 | height: ${metrics.baseUnit * 2 - 2}px; 52 | } 53 | } 54 | &.grow-exit.grow-exit-active:before, 55 | &.grow-exit.grow-exit-active:after { 56 | height: 0px; 57 | } 58 | `; 59 | 60 | const MenuButton = styled.div` 61 | height: ${metrics.headerHeight / 3}px; 62 | width: ${metrics.headerHeight / 3}px; 63 | z-index: 30; 64 | `; 65 | 66 | const Close = props => { 67 | return ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | }; 77 | 78 | export default Close; 79 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Form = styled.form` 4 | display: block; 5 | `; 6 | 7 | export default Form; 8 | -------------------------------------------------------------------------------- /src/components/H1.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | 5 | const P = styled.h1` 6 | font-size: ${metrics.H1}px; 7 | line-height: 1.5; 8 | margin: 0; 9 | font-weight: 700; 10 | position: relative; 11 | top: 8px; 12 | @media (max-width: 480px) { 13 | font-size: ${metrics.H1Mobile}px; 14 | } 15 | `; 16 | 17 | const Container = styled.div` 18 | margin: ${metrics.baseUnit * 3}px 0px; 19 | `; 20 | 21 | const H1 = props => { 22 | return ( 23 | 24 |

{props.children}

25 |
26 | ); 27 | }; 28 | 29 | export default H1; 30 | -------------------------------------------------------------------------------- /src/components/H2.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { metrics } from "../themes"; 3 | 4 | const H2 = styled.h2` 5 | font-size: ${metrics.H2}px; 6 | line-height: 1.5; 7 | margin: 0; 8 | margin-bottom: ${metrics.baseUnit * 2}px; 9 | font-weight: 700; 10 | position: relative; 11 | @media (max-width: 480px) { 12 | font-size: ${metrics.regularText}px; 13 | } 14 | `; 15 | 16 | export default H2; 17 | -------------------------------------------------------------------------------- /src/components/Hamburger.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 3 | import styled from "styled-components"; 4 | import { metrics } from "../themes"; 5 | 6 | const HamburgerStyle = styled.div` 7 | position: absolute; 8 | flex-direction: column; 9 | justify-content: center; 10 | height: ${metrics.headerHeight / 3}px; 11 | width: ${metrics.headerHeight / 3}px; 12 | div { 13 | flex-direction: column; 14 | width: 100%; 15 | } 16 | div :nth-child(1), 17 | div :nth-child(2) { 18 | margin-bottom: ${metrics.baseUnit - 2}px; 19 | } 20 | span { 21 | width: 100%; 22 | height: 0; 23 | border-top: 1px solid ${props => props.theme.mainText}; 24 | } 25 | &.grow-appear, 26 | &.grow-enter { 27 | width: 0px; 28 | z-index: 1; 29 | } 30 | &.grow-appear-active, 31 | &.grow-enter.grow-enter-active { 32 | width: ${metrics.headerHeight / 3}px; 33 | transition: width 400ms ease-out; 34 | @media (max-width: 480px) { 35 | width: ${metrics.headerHeight / 2}px; 36 | } 37 | } 38 | &.grow-exit { 39 | width: ${metrics.headerHeight / 3}px; 40 | @media (max-width: 480px) { 41 | width: ${metrics.headerHeight / 2}px; 42 | } 43 | } 44 | &.grow-exit.grow-exit-active { 45 | width: 0px; 46 | } 47 | `; 48 | 49 | const MenuButton = styled.div` 50 | height: ${metrics.headerHeight / 3}px; 51 | width: ${metrics.headerHeight / 3}px; 52 | z-index: 30; 53 | `; 54 | 55 | const Hamburger = props => { 56 | return ( 57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Hamburger; 74 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | 5 | const Link = styled.a` 6 | height: ${metrics.baseUnit * 3}px; 7 | width: ${metrics.baseUnit * 3}px; 8 | margin-left: ${props => (props.marginLeft ? metrics.baseUnit + "px" : 0)}; 9 | margin-right: ${props => (props.marginRight ? metrics.baseUnit + "px" : 0)}; 10 | margin-bottom: ${props => (props.marginBottom ? metrics.baseUnit + "px" : 0)}; 11 | margin-top: ${props => (props.marginTop ? metrics.baseUnit + "px" : 0)}; 12 | cursor: pointer; 13 | img { 14 | height: ${metrics.baseUnit * 3}px; 15 | width: ${metrics.baseUnit * 3}px; 16 | src: url(${props => props.src}); 17 | } 18 | `; 19 | 20 | const wrappedIcon = props => { 21 | return ( 22 | 23 | icon 24 | 25 | ); 26 | }; 27 | 28 | export default wrappedIcon; 29 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { metrics } from "../themes"; 3 | 4 | const Input = styled.input` 5 | background-color: transparent; 6 | color: ${props => props.theme.mainText}; 7 | border: none; 8 | border-radius: 0; 9 | border-bottom: 1px solid ${props => props.theme.inactive}; 10 | outline: none; 11 | margin: none; 12 | margin-bottom: ${metrics.baseUnit}px; 13 | padding: 0; 14 | height: ${metrics.baseUnit * 3 - 1}px; 15 | width: ${metrics.baseUnit * 20}px; 16 | font-size: ${metrics.regularText}px; 17 | &::placeholder { 18 | color: ${props => props.theme.inactive}; 19 | } 20 | &:focus { 21 | border-bottom: 1px solid ${props => props.theme.mainText}; 22 | } 23 | box-sizing: content-box; 24 | @media (max-width: 480px) { 25 | width: 100%; 26 | } 27 | `; 28 | 29 | export default Input; 30 | -------------------------------------------------------------------------------- /src/components/Message.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { metrics } from "../themes"; 3 | 4 | const Message = styled.div` 5 | position: relative; 6 | z-index: 42; 7 | font-size: ${metrics.H2}px; 8 | font-weight: 700; 9 | color: ${props => props.theme.overlayDetail}; 10 | padding: ${metrics.bodyPadding}px; 11 | `; 12 | 13 | export default Message; 14 | -------------------------------------------------------------------------------- /src/components/MobileMenuBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { Overlay } from "../components"; 4 | import MenuOverlay from "../containers/MenuOverlay"; 5 | import { withRouter } from "react-router-dom"; 6 | import { metrics } from "../themes"; 7 | 8 | const Container = styled.div` 9 | display: none; 10 | z-index: 40; 11 | width: 100%; 12 | justify-content: center; 13 | align-items: center; 14 | background-color: ${props => 15 | props.menuOpen ? props.theme.overlayBackground : props.theme.background}; 16 | bottom: 0; 17 | position: absolute; 18 | border-top: 1px solid 19 | ${props => 20 | props.menuOpen ? props.theme.overlayDetail : props.theme.inactive}; 21 | height: ${metrics.mobileMenuHeight - 1}px; 22 | @media (max-width: 480px) { 23 | display: flex; 24 | } 25 | `; 26 | 27 | const MobileMenuBar = props => { 28 | const [menuOpen, setMenuOpen] = useState(false); 29 | 30 | const menu = () => { 31 | return ( 32 | 33 | setMenuOpen(false)} /> 34 | 35 | ); 36 | }; 37 | 38 | return ( 39 | <> 40 | {menuOpen && menu()} 41 | 42 |
setMenuOpen(!menuOpen)}> 43 | {menuOpen ? "Close Menu" : "Open Menu"} 44 |
45 |
46 | 47 | ); 48 | }; 49 | 50 | export default withRouter(MobileMenuBar); 51 | -------------------------------------------------------------------------------- /src/components/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Background = styled.div` 5 | position: absolute; 6 | z-index: 10; 7 | height: 100%; 8 | width: 100%; 9 | background-color: ${props => props.theme.overlayBackground}; 10 | `; 11 | 12 | const Overlay = props => { 13 | return {props.children}; 14 | }; 15 | 16 | export default Overlay; 17 | -------------------------------------------------------------------------------- /src/components/P.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { metrics } from "../themes"; 3 | 4 | const P = styled.div` 5 | margin-bottom: ${metrics.baseUnit * 3}px; 6 | line-height: 2; 7 | font-size: ${metrics.regularText}px; 8 | @media (max-width: 480px) { 9 | font-size: ${metrics.smallText}px; 10 | } 11 | `; 12 | 13 | export default P; 14 | -------------------------------------------------------------------------------- /src/components/SocialAuthButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Button } from "../components"; 4 | import { metrics, icons } from "../themes"; 5 | 6 | const AuthButton = styled(Button)` 7 | span { 8 | width: ${metrics.baseUnit * 16.5}px; 9 | } 10 | `; 11 | 12 | const AuthIcon = styled.div` 13 | width: ${metrics.baseUnit * 2.5}px; 14 | height: ${metrics.baseUnit * 4}px; 15 | display: flex; 16 | justify-content: flex-end; 17 | align-items: center; 18 | div { 19 | width: ${metrics.baseUnit * 2}px; 20 | height: ${metrics.baseUnit * 2}px; 21 | background-color: ${props => 22 | props.background ? props.theme.detailText : null}; 23 | border-radius: ${metrics.globalBorderRadius / 2}px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | img { 28 | width: ${props => 29 | props.background ? metrics.baseUnit * 1.5 : metrics.baseUnit * 2}px; 30 | height: ${props => 31 | props.background ? metrics.baseUnit * 1.5 : metrics.baseUnit * 2}px; 32 | src: ${props => props.src}; 33 | } 34 | } 35 | `; 36 | 37 | const SocialConstructor = props => { 38 | return ( 39 | 40 | 41 |
42 | social-icon 43 |
44 |
45 | Sign in with {props.company} 46 |
47 | ); 48 | }; 49 | 50 | export const FacebookAuth = props => { 51 | return ( 52 | 58 | ); 59 | }; 60 | 61 | export const GoogleAuth = props => { 62 | return ( 63 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { keyframes } from "styled-components"; 3 | import { metrics } from "../themes"; 4 | 5 | const rotate = keyframes` 6 | from { 7 | transform: rotate(0deg); 8 | } 9 | 10 | to { 11 | transform: rotate(360deg); 12 | } 13 | `; 14 | 15 | const SpinnerTemplate = styled.div` 16 | border-radius: 50%; 17 | width: ${props => 18 | props.large ? metrics.baseUnit * 8 + 6 : metrics.baseUnit * 1.5}px; 19 | height: ${props => 20 | props.large ? metrics.baseUnit * 8 + 6 : metrics.baseUnit * 1.5}px; 21 | position: absolute; 22 | `; 23 | 24 | const Circle = styled(SpinnerTemplate)` 25 | border: 3px solid 26 | ${props => 27 | (props.secondary && props.theme.inactive) || props.theme.detailText}; 28 | opacity: 0.33; 29 | visibility: visible; 30 | `; 31 | 32 | const Highlight = styled(SpinnerTemplate)` 33 | border: 3px solid rgba(0, 0, 0, 0); 34 | border-top: 3px solid 35 | ${props => 36 | (props.secondary && props.theme.inactive) || props.theme.detailText}; 37 | animation: ${rotate} 1s infinite ease-in-out; 38 | `; 39 | 40 | const Spinner = props => { 41 | return ( 42 | <> 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Spinner; 50 | -------------------------------------------------------------------------------- /src/components/Switch.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | 5 | const Switched = styled.label` 6 | position: relative; 7 | display: inline-block; 8 | width: ${metrics.baseUnit * 6}px; 9 | height: ${metrics.baseUnit * 3}px; 10 | input { 11 | opacity: 0; 12 | width: 0; 13 | height: 0; 14 | } 15 | span { 16 | position: absolute; 17 | cursor: pointer; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | background-color: ${props => props.theme.overlayDetail}; 23 | transition: 0.4s; 24 | border-radius: ${metrics.baseUnit * 3}px; 25 | &:before { 26 | position: absolute; 27 | content: ""; 28 | height: ${metrics.baseUnit * 2}px; 29 | width: ${metrics.baseUnit * 2}px; 30 | left: ${metrics.baseUnit / 2}px; 31 | bottom: ${metrics.baseUnit / 2}px; 32 | background-color: ${props => props.theme.detailText}; 33 | transition: 0.4s; 34 | border-radius: 50%; 35 | } 36 | } 37 | input:checked + span { 38 | background-color: ${props => props.theme.overlayDetail}; 39 | } 40 | 41 | input:checked + span:before { 42 | transform: translateX(${metrics.baseUnit * 3}px); 43 | } 44 | `; 45 | 46 | const Switch = props => { 47 | return ( 48 | 49 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Switch; 60 | -------------------------------------------------------------------------------- /src/components/Toast.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { ToastContext } from "../contexts/toastContext"; 4 | import { UserContext } from "../contexts/userContext"; 5 | import { metrics } from "../themes"; 6 | import Transition from "react-transition-group/Transition"; 7 | 8 | const transitionStyles = { 9 | entered: { 10 | transform: `translateY(-${metrics.baseUnit * 9}px)`, 11 | transition: `transform 400ms ease-in-out` 12 | }, 13 | exiting: { 14 | transform: "translateY(0px)", 15 | transition: `transform 400ms ease-in-out` 16 | }, 17 | exited: { 18 | transform: "translateY(0px)" 19 | } 20 | }; 21 | 22 | const ToastContainer = styled.div` 23 | z-index: 2; 24 | width: 100%; 25 | display: flex; 26 | justify-content: center; 27 | position: absolute; 28 | bottom: -${metrics.baseUnit * 7}px; 29 | pointer-events: none; 30 | @media (max-width: 480px) { 31 | bottom: ${props => 32 | -(metrics.baseUnit * 8) + (props.loggedIn && metrics.mobileMenuHeight)}px; 33 | } 34 | div { 35 | padding: 0px ${metrics.baseUnit * 2}px; 36 | width: ${metrics.bodyWidth}px; 37 | display: flex; 38 | justify-content: flex-end; 39 | font-size: ${metrics.regularText}px; 40 | @media (max-width: 480px) { 41 | width: 100%; 42 | padding: 0; 43 | margin: 0px ${metrics.baseUnit}px; 44 | font-size: ${metrics.smallText}px; 45 | } 46 | div { 47 | visibility: ${props => props.visibility}; 48 | border-radius: ${metrics.globalBorderRadius}px; 49 | padding: 0; 50 | width: ${metrics.bodyWidth / 2}px; 51 | line-height: ${metrics.baseUnit * 2}px; 52 | color: ${props => props.theme.detailText}; 53 | background-color: ${props => props.theme.secondaryColor}; 54 | display: flex; 55 | justify-content: center; 56 | align-content: center; 57 | font-weight: 700; 58 | div { 59 | color: ${props => props.theme.detailText}; 60 | padding: ${metrics.baseUnit * 2}px; 61 | width: 100%; 62 | text-align: center; 63 | @media (max-width: 480px) { 64 | padding: ${metrics.baseUnit}px; 65 | } 66 | } 67 | } 68 | } 69 | `; 70 | 71 | const ToastWithContext = props => { 72 | const [show, setShow] = useState(false); 73 | const { message, sendMessage } = useContext(ToastContext); 74 | const { userState } = useContext(UserContext); 75 | 76 | useEffect(() => { 77 | if (message) { 78 | setShow(true); 79 | setTimeout(() => { 80 | setShow(false); 81 | }, 4000); 82 | } 83 | }, [message]); 84 | 85 | const onRest = () => { 86 | !show && sendMessage(""); 87 | }; 88 | 89 | return ( 90 | 91 | {motionState => ( 92 | 100 |
101 |
102 |
{message}
103 |
104 |
105 |
106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default ToastWithContext; 112 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Input from "./Input"; 2 | import Button from "./Button"; 3 | import H1 from "./H1"; 4 | import H2 from "./H2"; 5 | import P from "./P"; 6 | import Message from "./Message"; 7 | import Toast from "./Toast"; 8 | import CenteredDiv from "./CenteredDiv"; 9 | import Icon from "./Icon"; 10 | import Form from "./Form"; 11 | import BodyWrapper from "./BodyWrapper"; 12 | import MobileMenuBar from "./MobileMenuBar"; 13 | import Spinner from "./Spinner"; 14 | import Hamburger from "./Hamburger"; 15 | import Close from "./Close"; 16 | import Switch from "./Switch"; 17 | import Overlay from "./Overlay"; 18 | import { GoogleAuth, FacebookAuth } from "./SocialAuthButton"; 19 | 20 | export { 21 | Input, 22 | Button, 23 | H1, 24 | H2, 25 | P, 26 | Message, 27 | Toast, 28 | CenteredDiv, 29 | Switch, 30 | Icon, 31 | Spinner, 32 | Hamburger, 33 | Close, 34 | Form, 35 | Overlay, 36 | GoogleAuth, 37 | FacebookAuth, 38 | BodyWrapper, 39 | MobileMenuBar 40 | }; 41 | -------------------------------------------------------------------------------- /src/containers/Confirmed.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { Button, CenteredDiv, Message, Overlay } from "../components"; 4 | import firebase from "../firebase.js"; 5 | import "firebase/firestore"; 6 | 7 | const Confirmed = props => { 8 | const [newDevice, setNewDevice] = useState(false); 9 | const [complete, setComplete] = useState(false); 10 | const db = firebase.firestore(); 11 | 12 | useEffect(() => { 13 | let email = window.localStorage.getItem("confirmationEmail"); 14 | if (!email) { 15 | setNewDevice(true); 16 | } else { 17 | finishConfirmation(email); 18 | } 19 | }); 20 | 21 | const finishConfirmation = confirmedEmail => { 22 | if (firebase.auth().isSignInWithEmailLink(window.location.href)) { 23 | firebase 24 | .auth() 25 | .signInWithEmailLink(confirmedEmail, window.location.href) 26 | .then(result => { 27 | if (result.additionalUserInfo.isNewUser) { 28 | db.collection("users") 29 | .doc(result.user.uid) 30 | .set({ 31 | email: confirmedEmail 32 | }) 33 | .then(res => { 34 | setComplete(true); 35 | }); 36 | } 37 | window.localStorage.removeItem("confirmationEmail"); 38 | return; 39 | }) 40 | .then(() => { 41 | setTimeout(() => { 42 | window.close(); 43 | }, 5000); 44 | }) 45 | .catch(error => { 46 | console.log(error); 47 | }); 48 | } 49 | }; 50 | 51 | const newDeviceCheck = () => { 52 | return ( 53 | 54 | 55 | It appears that you're trying to verify your account from a different 56 | device or browser than the one you began with. Please complete the 57 | verification process from the same device/browser you started from. 58 | 59 | 66 | 67 | ); 68 | }; 69 | 70 | const confirmed = () => { 71 | return ( 72 | 73 | You are now confirmed! Navigate back to the app! 74 | 75 | ); 76 | }; 77 | 78 | const confirmationCheck = () => { 79 | if ( 80 | newDevice === true && 81 | complete !== true && 82 | !firebase.auth().currentUser 83 | ) { 84 | return newDeviceCheck(); 85 | } else { 86 | return confirmed(); 87 | } 88 | }; 89 | 90 | return {confirmationCheck()}; 91 | }; 92 | 93 | export default withRouter(Confirmed); 94 | -------------------------------------------------------------------------------- /src/containers/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from "react"; 2 | import { P, H1, Button, Input, Form, BodyWrapper } from "../components"; 3 | import { UserContext } from "../contexts/userContext"; 4 | import { ToastContext } from "../contexts/toastContext"; 5 | import firebase from "../firebase.js"; 6 | import "firebase/firestore"; 7 | 8 | const Dashboard = () => { 9 | const [firstName, setFirstName] = useState(null); 10 | const [lastName, setLastName] = useState(null); 11 | const [moreInfoComplete, setMoreInfoComplete] = useState(false); 12 | const { userState, userDispatch } = useContext(UserContext); 13 | const { sendMessage } = useContext(ToastContext); 14 | const db = firebase.firestore(); 15 | 16 | useEffect(() => { 17 | if ( 18 | (moreInfoComplete || userState.userData.firstName) && 19 | "Notification" in window && 20 | Notification.permission === "default" 21 | ) { 22 | requestNotifications(); 23 | } 24 | }, []); 25 | 26 | const requestNotifications = () => { 27 | Notification.requestPermission().then(permission => { 28 | if (permission === "granted") { 29 | const messaging = firebase.messaging(); 30 | messaging 31 | .getToken() 32 | .then(currentToken => { 33 | db.collection("users") 34 | .doc(firebase.auth().currentUser.uid) 35 | .set({ pushTokenWeb: currentToken }, { merge: true }) 36 | .then(() => { 37 | sendMessage("Notifications activated!"); 38 | }) 39 | .catch(err => console.log(err)); 40 | }) 41 | .catch(err => { 42 | console.log("An error occurred while retrieving token.", err); 43 | }); 44 | } 45 | }); 46 | }; 47 | 48 | const onClickSubmit = e => { 49 | e.preventDefault(); 50 | if (firstName && lastName) { 51 | db.collection("users") 52 | .doc(firebase.auth().currentUser.uid) 53 | .set( 54 | { 55 | firstName: firstName, 56 | lastName: lastName 57 | }, 58 | { merge: true } 59 | ) 60 | .then(() => { 61 | userDispatch({ 62 | type: "updateProfile", 63 | payload: { 64 | firstName: firstName, 65 | lastName: lastName 66 | } 67 | }); 68 | setMoreInfoComplete(true); 69 | sendMessage("Welcome!"); 70 | requestNotifications(); 71 | }); 72 | } else { 73 | sendMessage("Please complete the form."); 74 | } 75 | }; 76 | 77 | const moreInfo = () => { 78 | return ( 79 | 80 |

Onboarding

81 |

82 | This is an introduction screen that shows up after the user 83 | successfully logs in for the first time. It's a good opportunity to 84 | collect additional information or provide them with important details 85 | about how your application works. 86 |

87 |
88 |
89 | setFirstName(e.target.value)} 91 | name="firstName" 92 | placeholder="First name" 93 | autoComplete="given-name" 94 | /> 95 |
96 |
97 | setLastName(e.target.value)} 99 | name="lastName" 100 | placeholder="Last name" 101 | autoComplete="family-name" 102 | /> 103 |
104 | 105 |
106 |
107 | ); 108 | }; 109 | 110 | const dashboard = () => { 111 | return ( 112 | 113 |

Dashboard

114 |

115 | So this is your dashboard. Maybe you'll put a few graphs here, you've 116 | always wanted to try out D3. Maybe a news feed, or updates on new 117 | features. 118 |

119 |

120 | So this is your dashboard. Maybe you'll put a few graphs here, you've 121 | always wanted to try out D3. Maybe a news feed, or updates on new 122 | features. So this is your dashboard. Maybe you'll put a few graphs 123 | here, you've always wanted to try out D3. Maybe a news feed, or 124 | updates on new features. So this is your dashboard. Maybe you'll put a 125 | few graphs here, you've always wanted to try out D3. Maybe a news 126 | feed, or updates on new features. 127 |

128 |

129 | So this is your dashboard. Maybe you'll put a few graphs here, you've 130 | always wanted to try out D3. Maybe a news feed, or updates on new 131 | features. 132 |

133 |

134 | So this is your dashboard. Maybe you'll put a few graphs here, you've 135 | always wanted to try out D3. Maybe a news feed, or updates on new 136 | features. So this is your dashboard. Maybe you'll put a few graphs 137 | here, you've always wanted to try out D3. Maybe a news feed, or 138 | updates on new features. 139 |

140 |

141 | So this is your dashboard. Maybe you'll put a few graphs here, you've 142 | always wanted to try out D3. Maybe a news feed, or updates on new 143 | features. 144 |

145 |

146 | So this is your dashboard. Maybe you'll put a few graphs here, you've 147 | always wanted to try out D3. Maybe a news feed, or updates on new 148 | features. So this is your dashboard. Maybe you'll put a few graphs 149 | here, you've always wanted to try out D3. Maybe a news feed, or 150 | updates on new features. 151 |

152 |

153 | So this is your dashboard. Maybe you'll put a few graphs here, you've 154 | always wanted to try out D3. Maybe a news feed, or updates on new 155 | features. 156 |

157 |
158 | ); 159 | }; 160 | return moreInfoComplete || userState.userData.firstName 161 | ? dashboard() 162 | : moreInfo(); 163 | }; 164 | 165 | export default Dashboard; 166 | -------------------------------------------------------------------------------- /src/containers/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { Overlay, Hamburger } from "../components"; 4 | import MenuOverlay from "../containers/MenuOverlay"; 5 | import { UserContext } from "../contexts/userContext"; 6 | import styled from "styled-components"; 7 | import { metrics } from "../themes"; 8 | 9 | const Header = styled.div` 10 | transform: translateY(${props => "-" + props.scrollTop}px); 11 | z-index: 8; 12 | position: absolute; 13 | background-color: ${props => props.theme.background}; 14 | width: 100%; 15 | height: ${metrics.headerHeight - 1}px; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | border-bottom: 1px solid ${props => props.theme.inactive}; 20 | @media (max-width: 480px) { 21 | display: none; 22 | } 23 | `; 24 | 25 | const HeaderInner = styled.div` 26 | width: ${metrics.bodyWidth}px; 27 | padding: 0px ${metrics.baseUnit * 2}px; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | div { 32 | display: inline-flex; 33 | } 34 | `; 35 | 36 | const CompanyLogo = styled.button` 37 | color: ${props => props.theme.mainText}; 38 | background-color: transparent; 39 | pointer-events: ${props => (props.samePath ? "none" : "initial")}; 40 | border: 0; 41 | outline: none; 42 | padding: 0; 43 | cursor: pointer; 44 | outline: 0; 45 | font-size: ${metrics.smallText}px; 46 | `; 47 | 48 | const HeaderWithRouter = props => { 49 | const [menuOpen, setMenuOpen] = useState(false); 50 | const { userState } = useContext(UserContext); 51 | const userId = userState.userId; 52 | const pushTo = path => { 53 | props.location.pathname !== path && props.history.push(path); 54 | }; 55 | 56 | const toggleMenu = () => { 57 | menuOpen ? setMenuOpen(false) : setMenuOpen(true); 58 | }; 59 | 60 | const menuButtonState = () => { 61 | return menuOpen ? null : ; 62 | }; 63 | 64 | const menu = () => { 65 | return ( 66 | 67 | setMenuOpen(false)} /> 68 | 69 | ); 70 | }; 71 | 72 | return ( 73 | <> 74 | {menuOpen && menu()} 75 |
76 | 77 | (userId ? pushTo("/dashboard") : pushTo("/"))} 79 | > 80 | React Firebase Essentials 81 | 82 | {userId && menuButtonState()} 83 | 84 |
85 | 86 | ); 87 | }; 88 | 89 | export default withRouter(HeaderWithRouter); 90 | -------------------------------------------------------------------------------- /src/containers/Lander.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { H1, H2, P, Button, BodyWrapper } from "../components"; 3 | import { withRouter } from "react-router-dom"; 4 | 5 | const Lander = props => { 6 | const signIn = () => { 7 | props.history.push("/signin"); 8 | }; 9 | 10 | return ( 11 | 12 |

React Firebase Essentials

13 |

14 | A fully-featured single-page app template, hosted and running in 15 | minutes. Based on Create-React-App and Firebase. Written with 16 | React-Hooks. 17 |

18 |

19 |

  • 20 | Modern authentication and onboarding UI flows have already been built 21 | out, it's as simple as plugging in your Firebase API keys. 22 |
  • 23 |
  • Mobile-ready responsive design.
  • 24 |
  • 25 | Utilizes Styled-Components, carefully using global variables that 26 | allow you to quickly and easily adjust to your tastes. 27 |
  • 28 |
  • 29 | It's built entirely with React Hooks and the new Context API. Built on 30 | top of Create-React-App. 31 |
  • 32 |
  • 33 | Requires very few dependencies. The least popular module it uses has 34 | 6.4k stars on Github. 35 |
  • 36 |
  • 37 | Basic security rules have already been written for the Firestore DB. 38 |
  • 39 |
  • 40 | Push Notifications set up out of the box, with a cloud function 41 | supplied for triggering messages. 42 |
  • 43 |
  • Dark Mode! You gotta have dark mode!
  • 44 |

    45 | 46 |
    47 | ); 48 | }; 49 | 50 | export default withRouter(Lander); 51 | -------------------------------------------------------------------------------- /src/containers/MenuOverlay.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import firebase from "../firebase.js"; 3 | import { withRouter } from "react-router-dom"; 4 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 5 | import { UserContext } from "../contexts/userContext"; 6 | import { Switch, Close } from "../components"; 7 | import styled from "styled-components"; 8 | import { metrics } from "../themes"; 9 | 10 | const Container = styled.div` 11 | height: 100%; 12 | width: 100%; 13 | display: flex; 14 | justify-content: center; 15 | @media (max-width: 480px) { 16 | align-items: center; 17 | } 18 | `; 19 | 20 | const MenuHeader = styled.div` 21 | width: 100%; 22 | height: ${metrics.headerHeight - 1}px; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | border-bottom: 1px solid ${props => props.theme.overlayDetail}; 27 | @media (max-width: 480px) { 28 | display: none; 29 | } 30 | `; 31 | 32 | const HeaderInner = styled.div` 33 | width: ${metrics.bodyWidth}px; 34 | padding: 0px ${metrics.baseUnit * 2}px; 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | div { 39 | display: inline-flex; 40 | } 41 | `; 42 | 43 | const BackgroundInner = styled.div` 44 | padding: 0px ${metrics.bodyPadding}px; 45 | width: ${metrics.bodyWidth}px; 46 | @media (max-width: 480px) { 47 | margin-bottom: ${metrics.mobileMenuHeight}px; 48 | } 49 | `; 50 | 51 | const MenuItem = styled.button` 52 | position: relative; 53 | display: block; 54 | background-color: transparent; 55 | color: ${props => (props.samePath ? props.theme.overlayDetail : "inherit")}; 56 | pointer-events: ${props => (props.samePath ? "none" : "initial")}; 57 | border: 0; 58 | margin-top: ${metrics.baseUnit * 3}px; 59 | height: ${metrics.baseUnit * 6}px; 60 | width: 100%; 61 | text-align: left; 62 | outline: none; 63 | padding: 0; 64 | font-size: ${metrics.H1}px; 65 | line-height: 1.5; 66 | top: 2px; 67 | cursor: pointer; 68 | outline: 0; 69 | font-weight: 700; 70 | &:last-child { 71 | display: none; 72 | } 73 | @media (max-width: 480px) { 74 | font-size: ${metrics.H2}px; 75 | margin-top: 0; 76 | line-height: ${metrics.baseUnit * 3}px; 77 | height: 1; 78 | text-align: center; 79 | &:last-child { 80 | display: initial; 81 | } 82 | } 83 | &.fade-appear, 84 | &.fade-enter { 85 | opacity: 0; 86 | transform: translateY(24px); 87 | } 88 | &.fade-appear-active, 89 | &.fade-enter.fade-enter-active { 90 | opacity: 1; 91 | transform: translateY(0); 92 | transition: opacity 400ms linear 400ms, transform 400ms ease-out 400ms; 93 | transition-delay: ${props => props.child * 0.2}s; 94 | } 95 | &.switch-appear, 96 | &.switch-enter { 97 | opacity: 0; 98 | } 99 | &.switch-appear-active, 100 | &.switch-enter.switch-enter-active { 101 | opacity: 1; 102 | transition: opacity 400ms linear 400ms, transform 400ms ease-out 400ms; 103 | transition-delay: ${props => props.child * 0.2}s; 104 | } 105 | `; 106 | 107 | const MenuOverlay = props => { 108 | const { userDispatch } = useContext(UserContext); 109 | const styleMode = window.localStorage.getItem("styleMode"); 110 | 111 | const signOut = () => { 112 | props.setMenuOpen(); 113 | firebase.auth().signOut(); 114 | userDispatch({ 115 | type: "userId", 116 | payload: null 117 | }); 118 | }; 119 | 120 | const pushTo = path => { 121 | props.setMenuOpen(); 122 | !samePath(path) && props.history.push(path); 123 | }; 124 | 125 | const samePath = path => { 126 | return props.location.pathname === path; 127 | }; 128 | 129 | const toggleStyles = () => { 130 | const newStyle = styleMode === "main" ? "dark" : "main"; 131 | userDispatch({ 132 | type: "styleMode", 133 | payload: newStyle 134 | }); 135 | window.localStorage.setItem("styleMode", newStyle); 136 | }; 137 | 138 | return ( 139 | <> 140 | 141 | 142 | 143 | props.setMenuOpen()} /> 144 | 145 | 146 | 147 | 148 | 149 | 150 | pushTo("/dashboard")} 153 | > 154 | Dashboard 155 | 156 | 157 | 158 | pushTo("/profile")} 161 | > 162 | Profile 163 | 164 | 165 | 166 | { 168 | window.location.href = 169 | "http://github.com/eemebarbe/react-firebase-essentials"; 170 | }} 171 | > 172 | Github 173 | 174 | 175 | 176 | Sign out 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | ); 186 | }; 187 | 188 | export default withRouter(MenuOverlay); 189 | -------------------------------------------------------------------------------- /src/containers/PrivacyPolicy.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { metrics } from "../themes"; 4 | import { H1, H2, P, BodyWrapper } from "../components"; 5 | 6 | const OL = styled.ol` 7 | font-size: ${metrics.smallText}px; 8 | line-height: 2; 9 | margin-left: ${metrics.baseUnit * 3}px; 10 | padding-left: 0; 11 | margin-bottom: ${metrics.baseUnit * 3}px; 12 | `; 13 | 14 | const UL = styled.ul` 15 | font-size: ${metrics.smallText}px; 16 | line-height: 2; 17 | margin-left: ${metrics.baseUnit * 3}px; 18 | padding-left: 0; 19 | margin-bottom: ${metrics.baseUnit * 3}px; 20 | `; 21 | 22 | const PrivacyPolicy = () => { 23 | return ( 24 | 25 |

    Web Site Terms and Conditions of Use

    26 |

    1. Terms

    27 |

    28 | By accessing this web site, you are agreeing to be bound by these web 29 | site Terms and Conditions of Use, applicable laws and regulations and 30 | their compliance. If you disagree with any of the stated terms and 31 | conditions, you are prohibited from using or accessing this site. The 32 | materials contained in this site are secured by relevant copyright and 33 | trade mark law. 34 |

    35 |

    2. Use License

    36 |
      37 |
    1. 38 | Permission is allowed to temporarily download one duplicate of the 39 | materials (data or programming) on react-firebase-essentials.com's 40 | site for individual and non-business use only. This is the just a 41 | permit of license and not an exchange of title, and under this permit 42 | you may not: 43 |
        44 |
      1. modify or copy the materials;
      2. 45 |
      3. 46 | use the materials for any commercial use , or for any public 47 | presentation (business or non-business);{" "} 48 |
      4. 49 |
      5. 50 | attempt to decompile or rebuild any product or material contained 51 | on react-firebase-essentials.com's site; 52 |
      6. 53 |
      7. 54 | remove any copyright or other restrictive documentations from the 55 | materials; or 56 |
      8. 57 |
      9. 58 | transfer the materials to someone else or even "mirror" the 59 | materials on other server. 60 |
      10. 61 |
      62 |
    2. 63 |
    3. 64 | This permit might consequently be terminated if you disregard any of 65 | these confinements and may be ended by react-firebase-essentials.com 66 | whenever deemed. After permit termination or when your viewing permit 67 | is terminated, you must destroy any downloaded materials in your 68 | ownership whether in electronic or printed form. 69 |
    4. 70 |
    71 |

    3. Disclaimer

    72 |
      73 |
    1. 74 | The materials on react-firebase-essentials.com's site are given "as 75 | is". react-firebase-essentials.com makes no guarantees, communicated 76 | or suggested, and thus renounces and nullifies every single other 77 | warranties, including without impediment, inferred guarantees or 78 | states of merchantability, fitness for a specific reason, or 79 | non-encroachment of licensed property or other infringement of rights. 80 | Further, react-firebase-essentials.com does not warrant or make any 81 | representations concerning the precision, likely results, or 82 | unwavering quality of the utilization of the materials on its Internet 83 | site or generally identifying with such materials or on any 84 | destinations connected to this website. 85 |
    2. 86 |
    87 |

    4. Constraints

    88 |

    89 | In no occasion should react-firebase-essentials.com or its suppliers 90 | subject for any harms (counting, without constraint, harms for loss of 91 | information or benefit, or because of business interference,) emerging 92 | out of the utilization or powerlessness to utilize the materials on 93 | react-firebase-essentials.com's Internet webpage, regardless of the 94 | possibility that react-firebase-essentials.com or a 95 | react-firebase-essentials.com approved agent has been told orally or in 96 | written of the likelihood of such harm. Since a few purviews don't 97 | permit constraints on inferred guarantees, or impediments of obligation 98 | for weighty or coincidental harms, these confinements may not make a 99 | difference to you. 100 |

    101 |

    5. Amendments and Errata

    102 |

    103 | The materials showing up on react-firebase-essentials.com's site could 104 | incorporate typographical, or photographic mistakes. 105 | react-firebase-essentials.com does not warrant that any of the materials 106 | on its site are exact, finished, or current. 107 | react-firebase-essentials.com may roll out improvements to the materials 108 | contained on its site whenever without notification. 109 | react-firebase-essentials.com does not, then again, make any dedication 110 | to update the materials. 111 |

    112 |

    6. Links

    113 |

    114 | react-firebase-essentials.com has not checked on the majority of the 115 | websites or links connected to its website and is not in charge of the 116 | substance of any such connected webpage. The incorporation of any 117 | connection does not infer support by react-firebase-essentials.com of 118 | the site. Utilization of any such connected site is at the user's own 119 | risk. 120 |

    121 |

    7. Site Terms of Use Modifications

    122 |

    123 | react-firebase-essentials.com may update these terms of utilization for 124 | its website whenever without notification. By utilizing this site you 125 | are consenting to be bound by the then current form of these Terms and 126 | Conditions of Use. 127 |

    128 |

    8. Governing Law

    129 |

    130 | Any case identifying with react-firebase-essentials.com's site should be 131 | administered by the laws of the country of United States 132 | react-firebase-essentials.com State without respect to its contention of 133 | law provisions. 134 |

    135 |

    General Terms and Conditions applicable to Use of a Web Site.

    136 |

    Privacy Policy

    137 |

    138 | Your privacy is critical to us. Likewise, we have built up this Policy 139 | with the end goal you should see how we gather, utilize, impart and 140 | reveal and make utilization of individual data. The following blueprints 141 | our privacy policy. 142 |

    143 |
      144 |
    • 145 | Before or at the time of collecting personal information, we will 146 | identify the purposes for which information is being collected. 147 |
    • 148 |
    • 149 | We will gather and utilization of individual data singularly with the 150 | target of satisfying those reasons indicated by us and for other good 151 | purposes, unless we get the assent of the individual concerned or as 152 | required by law. 153 |
    • 154 |
    • 155 | We will just hold individual data the length of essential for the 156 | satisfaction of those reasons. 157 |
    • 158 |
    • 159 | We will gather individual data by legal and reasonable means and, 160 | where fitting, with the information or assent of the individual 161 | concerned. 162 |
    • 163 |
    • 164 | Personal information ought to be important to the reasons for which it 165 | is to be utilized, and, to the degree essential for those reasons, 166 | ought to be exact, finished, and updated. 167 |
    • 168 |
    • 169 | We will protect individual data by security shields against misfortune 170 | or burglary, and also unapproved access, divulgence, duplicating, use 171 | or alteration. 172 |
    • 173 |
    • 174 | We will promptly provide customers with access to our policies and 175 | procedures for the administration of individual data. 176 |
    • 177 |
    178 |

    179 | We are focused on leading our business as per these standards with a 180 | specific end goal to guarantee that the privacy of individual data is 181 | secure and maintained. 182 |

    183 |
    184 | ); 185 | }; 186 | 187 | export default PrivacyPolicy; 188 | -------------------------------------------------------------------------------- /src/containers/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { P, H1, Button, Input, Form, BodyWrapper } from "../components"; 3 | import { ToastContext } from "../contexts/toastContext"; 4 | import { UserContext } from "../contexts/userContext"; 5 | import firebase from "../firebase.js"; 6 | import "firebase/firestore"; 7 | 8 | const Profile = () => { 9 | const { userState, userDispatch } = useContext(UserContext); 10 | const [firstName, setFirstName] = useState(userState.userData.firstName); 11 | const [lastName, setLastName] = useState(userState.userData.lastName); 12 | const [loadState, setloadState] = useState(false); 13 | const { sendMessage } = useContext(ToastContext); 14 | const db = firebase.firestore(); 15 | 16 | const onClickSubmit = e => { 17 | e.preventDefault(); 18 | if (firstName && lastName) { 19 | if ( 20 | firstName !== userState.userData.firstName || 21 | lastName !== userState.userData.lastName 22 | ) { 23 | setloadState(true); 24 | db.collection("users") 25 | .doc(firebase.auth().currentUser.uid) 26 | .update({ 27 | firstName: firstName, 28 | lastName: lastName 29 | }) 30 | .then(() => { 31 | setloadState(false); 32 | userDispatch({ 33 | type: "updateProfile", 34 | payload: { 35 | firstName: firstName, 36 | lastName: lastName 37 | } 38 | }); 39 | sendMessage("Update successful!"); 40 | }) 41 | .catch(err => { 42 | setloadState(false); 43 | sendMessage(err.message); 44 | }); 45 | } else { 46 | sendMessage("You didn't enter any new information."); 47 | } 48 | } else { 49 | sendMessage("You can't leave any of your information blank."); 50 | } 51 | }; 52 | 53 | return ( 54 | 55 |

    Profile

    56 |

    Update your personal information here.

    57 |
    58 |
    59 | setFirstName(e.target.value)} 61 | name="firstName" 62 | placeholder="First name" 63 | autoComplete="given-name" 64 | value={firstName} 65 | /> 66 |
    67 |
    68 | setLastName(e.target.value)} 70 | name="lastName" 71 | placeholder="Last name" 72 | autoComplete="family-name" 73 | value={lastName} 74 | /> 75 |
    76 |
    77 | 80 |
    81 |
    82 |
    83 | ); 84 | }; 85 | 86 | export default Profile; 87 | -------------------------------------------------------------------------------- /src/containers/SignIn.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { 3 | P, 4 | H1, 5 | Button, 6 | FacebookAuth, 7 | GoogleAuth, 8 | Input, 9 | Form, 10 | Overlay, 11 | CenteredDiv, 12 | Message, 13 | BodyWrapper 14 | } from "../components"; 15 | import { ToastContext } from "../contexts/toastContext"; 16 | import { UserContext } from "../contexts/userContext"; 17 | import firebase from "../firebase.js"; 18 | import "firebase/functions"; 19 | import "firebase/firestore"; 20 | import styled from "styled-components"; 21 | import { metrics } from "../themes"; 22 | 23 | const AuthSeparator = styled.div` 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | line-height: 1; 28 | font-size: ${metrics.smallText}px; 29 | height: ${metrics.baseUnit * 2}px; 30 | width: ${metrics.baseUnit * 20}px; 31 | margin-bottom: ${metrics.baseUnit}px; 32 | color: ${props => props.theme.inactive}; 33 | font-weight: 700; 34 | span { 35 | margin: 0px ${metrics.baseUnit}px; 36 | } 37 | @media (max-width: 480px) { 38 | width: 100%; 39 | } 40 | `; 41 | 42 | const SignIn = () => { 43 | const [facebookLoadState, setFacebookLoadState] = useState(false); 44 | const [googleLoadState, setGoogleLoadState] = useState(false); 45 | const [email, setEmail] = useState(""); 46 | const { sendMessage } = useContext(ToastContext); 47 | const { userState, userDispatch } = useContext(UserContext); 48 | const db = firebase.firestore(); 49 | 50 | const onClickSubmit = e => { 51 | e.preventDefault(); 52 | if (email && email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { 53 | window.localStorage.setItem("confirmationEmail", email); 54 | const actionCodeSettings = { 55 | url: "http://" + process.env.REACT_APP_BASE_URL + "/confirmed", 56 | handleCodeInApp: true 57 | }; 58 | firebase 59 | .auth() 60 | .sendSignInLinkToEmail(email, actionCodeSettings) 61 | .then(() => { 62 | userDispatch({ 63 | type: "verifying", 64 | payload: { 65 | verifying: true 66 | } 67 | }); 68 | }) 69 | .catch(error => { 70 | sendMessage(error.message); 71 | }); 72 | } else { 73 | sendMessage("Please enter a valid email address."); 74 | } 75 | }; 76 | 77 | const authWithFacebook = () => { 78 | setFacebookLoadState(true); 79 | const facebookProvider = new firebase.auth.FacebookAuthProvider(); 80 | firebase 81 | .auth() 82 | .signInWithPopup(facebookProvider) 83 | .then(result => { 84 | if (result.additionalUserInfo.isNewUser) { 85 | db.collection("users") 86 | .doc(result.user.uid) 87 | .set({ 88 | email: result.additionalUserInfo.profile.email 89 | }); 90 | } 91 | }) 92 | .catch(err => { 93 | if (err.code === "auth/account-exists-with-different-credential") { 94 | sendMessage( 95 | "It looks like the email address associated with your Facebook account has already been used to sign in with another method. Please sign in using the original method you signed up with." 96 | ); 97 | } else { 98 | sendMessage(err.message); 99 | } 100 | setFacebookLoadState(false); 101 | }); 102 | }; 103 | 104 | const authWithGoogle = () => { 105 | setGoogleLoadState(true); 106 | const googleProvider = new firebase.auth.GoogleAuthProvider(); 107 | firebase 108 | .auth() 109 | .signInWithPopup(googleProvider) 110 | .then(result => { 111 | if (result.additionalUserInfo.isNewUser) { 112 | db.collection("users") 113 | .doc(result.user.uid) 114 | .set({ 115 | email: result.additionalUserInfo.profile.email 116 | }); 117 | } 118 | }) 119 | .catch(err => { 120 | if (err.code === "auth/account-exists-with-different-credential") { 121 | sendMessage( 122 | "It looks like the email address associated with your Facebook account has already been used to sign in with another method. Please sign in using the original method you signed up with." 123 | ); 124 | } else { 125 | sendMessage(err.message); 126 | } 127 | setGoogleLoadState(false); 128 | }); 129 | }; 130 | 131 | const overlay = () => { 132 | return ( 133 | 134 | 135 | 136 | Please open the email we sent you, so we can verify your account! 137 | 138 | 139 | 140 | ); 141 | }; 142 | 143 | return ( 144 | <> 145 | {userState.verifying && overlay()} 146 | 147 |

    Sign Up/Sign In

    148 |

    149 | Signing in and signing up are the same process, and no password is 150 | asked for...hopefully you don't mind! I believe that verification by 151 | email is better for consumer trust in the era of so many data 152 | breaches, and is arguably safer than a traditional authentication 153 | setup. 154 |

    155 |
    156 |
    157 | setEmail(e.target.value)} 159 | name="email" 160 | placeholder="Email address" 161 | autoComplete="email" 162 | /> 163 |
    164 |
    165 | 168 |
    169 |
    170 | 171 | OR 172 | 173 | 178 | 179 |
    180 | 181 | ); 182 | }; 183 | 184 | export default SignIn; 185 | -------------------------------------------------------------------------------- /src/contexts/toastContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export const ToastContext = React.createContext(""); 4 | 5 | export const ToastProvider = props => { 6 | const [message, sendMessage] = useState(""); 7 | return ( 8 | 9 | {props.children} 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/contexts/userContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, createContext } from "react"; 2 | const styleMode = window.localStorage.getItem("styleMode"); 3 | 4 | const initialState = { 5 | userId: null, 6 | userData: { 7 | email: null, 8 | firstName: null, 9 | lastName: null, 10 | pushTokenWeb: null 11 | }, 12 | styleMode: styleMode ? styleMode : "main", 13 | verifying: false 14 | }; 15 | export const UserContext = createContext(initialState); 16 | 17 | const reducer = (state, action) => { 18 | switch (action.type) { 19 | case "userId": 20 | return { ...state, userId: action.payload }; 21 | case "verifying": 22 | return { ...state, verifying: action.payload }; 23 | case "updateProfile": 24 | return { 25 | ...state, 26 | userData: { 27 | ...state.userData, 28 | ...action.payload 29 | } 30 | }; 31 | case "styleMode": 32 | return { ...state, styleMode: action.payload }; 33 | case "signOut": 34 | return { ...initialState }; 35 | default: 36 | return state; 37 | } 38 | }; 39 | 40 | export const UserProvider = props => { 41 | const [userState, userDispatch] = useReducer(reducer, initialState); 42 | return ( 43 | 44 | {props.children} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | import * as firebase from "firebase/app"; 2 | import "firebase/messaging"; 3 | import "firebase/auth"; 4 | import "firebase/functions"; 5 | const config = { 6 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 7 | authDomain: process.env.REACT_APP_FIREBASE_DOMAIN, 8 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 9 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_ID 10 | }; 11 | firebase.initializeApp(config); 12 | 13 | if ("Notification" in window) { 14 | const messaging = firebase.messaging(); 15 | messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_MESSAGING_CERT); 16 | 17 | messaging.onMessage(payload => { 18 | console.log("Message received. ", payload); 19 | // push message to UI 20 | }); 21 | 22 | messaging.onTokenRefresh(() => { 23 | const db = firebase.firestore(); 24 | messaging 25 | .getToken() 26 | .then(refreshedToken => { 27 | db.collection("users") 28 | .doc(firebase.auth().currentUser.uid) 29 | .update({ pushTokenWeb: refreshedToken }) 30 | .then(() => { 31 | console.log("Token updated."); 32 | }) 33 | .catch(err => console.log(err)); 34 | }) 35 | .catch(err => { 36 | console.log("Unable to retrieve refreshed token ", err); 37 | }); 38 | }); 39 | } 40 | 41 | export default firebase; 42 | -------------------------------------------------------------------------------- /src/helpers/cloudFunctions.js: -------------------------------------------------------------------------------- 1 | import firebase from "../firebase.js"; 2 | 3 | export const sendPushNotification = data => { 4 | if (data.token) { 5 | const push = firebase.functions().httpsCallable("sendPushNotification"); 6 | push({ 7 | token: data.token, 8 | title: data.title, 9 | body: data.body 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eemebarbe/react-firebase-essentials/487d0edb0ae05238db41e0a444b9a8b01288bed4/src/index.css -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/themes/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { metrics } from "../themes"; 2 | import { createGlobalStyle } from "styled-components"; 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | @import url('https://fonts.googleapis.com/css?family=Questrial&display=swap'); 6 | html { 7 | height: 100%; 8 | width: 100%; 9 | overflow: hidden; 10 | font-size: ${metrics.baseUnit}px; 11 | background-color: ${props => props.theme.background}; 12 | color: ${props => props.theme.mainText}; 13 | @media (max-width: 480px){ 14 | font-size: ${metrics.baseUnit}px; 15 | } 16 | } 17 | body { 18 | input, textarea, button { 19 | font-family: inherit; 20 | } 21 | font-family: 'Questrial', sans-serif; 22 | margin: 0; 23 | padding: 0; 24 | -webkit-font-smooth: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | height: 100%; 27 | width: 100%; 28 | overflow: hidden; 29 | list-style-position: inside; 30 | -webkit-tap-highlight-color: rgba(0,0,0,0); 31 | input:-webkit-autofill, 32 | input:-webkit-autofill:hover, 33 | input:-webkit-autofill:focus, 34 | input:-webkit-autofill:active { 35 | -webkit-box-shadow: 0 0 0px 1000px ${props => 36 | props.theme.background} inset; 37 | box-shadow: 0 0 0px 1000px ${props => props.theme.background} inset; 38 | -webkit-text-fill-color: ${props => props.theme.mainText} !important; 39 | background-color: ${props => props.theme.background} !important; 40 | } 41 | } 42 | #root { 43 | height: 100%; 44 | }`; 45 | 46 | export default GlobalStyle; 47 | -------------------------------------------------------------------------------- /src/themes/colors.js: -------------------------------------------------------------------------------- 1 | const main = { 2 | inactive: "rgb(211, 211, 211)", 3 | primaryButton: "rgb(0, 0, 255)", 4 | mainText: "rgb(0, 0, 0)", 5 | detailText: "rgb(255, 255, 255)", 6 | secondaryColor: "rgb(255,0,0)", 7 | background: "rgb(255, 255, 255)", 8 | overlayBackground: "rgb(211, 211, 211)", 9 | overlayDetail: "rgb(169,169,169)" 10 | }; 11 | 12 | const dark = { 13 | inactive: "rgb(120, 120, 120)", 14 | primaryButton: "rgb(0, 0, 255)", 15 | mainText: "rgb(255, 255, 255)", 16 | detailText: "rgb(255, 255, 255)", 17 | secondaryColor: "rgb(255,0,0)", 18 | background: "rgb(40, 40, 40)", 19 | overlayBackground: "rgb(40, 40, 40)", 20 | overlayDetail: "rgb(68,68,68)" 21 | }; 22 | 23 | const colors = { 24 | main: main, 25 | dark: dark 26 | }; 27 | 28 | export default colors; 29 | -------------------------------------------------------------------------------- /src/themes/icons.js: -------------------------------------------------------------------------------- 1 | import facebook from "./icons/flogo-HexRBG-Wht-72.svg"; 2 | import google from "./icons/icons8-google-48.png"; 3 | import github from "./icons/github.svg"; 4 | 5 | const icons = { 6 | google: google, 7 | facebook: facebook, 8 | github: github 9 | }; 10 | 11 | export default icons; 12 | -------------------------------------------------------------------------------- /src/themes/icons/btn_google_signin_light_normal_web@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eemebarbe/react-firebase-essentials/487d0edb0ae05238db41e0a444b9a8b01288bed4/src/themes/icons/btn_google_signin_light_normal_web@2x.png -------------------------------------------------------------------------------- /src/themes/icons/flogo-HexRBG-Wht-72.svg: -------------------------------------------------------------------------------- 1 | flogo-HexRBG-Wht-72 -------------------------------------------------------------------------------- /src/themes/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/themes/icons/icons8-google-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eemebarbe/react-firebase-essentials/487d0edb0ae05238db41e0a444b9a8b01288bed4/src/themes/icons/icons8-google-48.png -------------------------------------------------------------------------------- /src/themes/icons/icons8-google-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eemebarbe/react-firebase-essentials/487d0edb0ae05238db41e0a444b9a8b01288bed4/src/themes/icons/icons8-google-96.png -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | import colors from "./colors"; 2 | import metrics from "./metrics"; 3 | import icons from "./icons"; 4 | 5 | export { colors, metrics, icons }; 6 | -------------------------------------------------------------------------------- /src/themes/metrics.js: -------------------------------------------------------------------------------- 1 | const metrics = { 2 | baseUnit: 12, 3 | baseFontUnit: 12, 4 | globalBorderRadius: 4, 5 | get bodyWidth() { 6 | return this.baseUnit * 64; 7 | }, 8 | get bodyPadding() { 9 | return this.baseUnit * 2; 10 | }, 11 | get mobileMenuHeight() { 12 | return this.baseUnit * 4; 13 | }, 14 | get headerHeight() { 15 | return this.baseUnit * 6; 16 | }, 17 | get smallText() { 18 | return this.baseFontUnit * 1.25; 19 | }, 20 | get regularText() { 21 | return this.baseFontUnit * 1.5; 22 | }, 23 | get H1() { 24 | return this.baseFontUnit * 4; 25 | }, 26 | get H2() { 27 | return this.baseFontUnit * 2; 28 | }, 29 | get H1Mobile() { 30 | return this.baseFontUnit * 3.5; 31 | }, 32 | animationLength: 400 33 | }; 34 | 35 | export default metrics; 36 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | --------------------------------------------------------------------------------