├── .env.sample ├── .eslintrc.json ├── .firebaserc ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── config-overrides.js ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .gitignore ├── package.json ├── src │ ├── createUserChannel.ts │ ├── getUserToken.ts │ ├── incrementUserScore.ts │ ├── index.ts │ ├── removeUserChannel.ts │ └── sendSMSMessage.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── global.d.ts ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── App.tsx ├── assets │ └── icons │ │ ├── check.svg │ │ ├── checkbox.svg │ │ ├── chevron_left.svg │ │ ├── circle_ring.svg │ │ ├── circle_star.svg │ │ ├── close.svg │ │ ├── google.svg │ │ ├── logo.svg │ │ ├── mascot.svg │ │ ├── mascot_x3.svg │ │ ├── person.svg │ │ ├── plus.svg │ │ ├── plus_large.svg │ │ ├── speech_bubble.svg │ │ ├── squares.svg │ │ ├── text_bubble.svg │ │ ├── trash.svg │ │ └── trophy.svg ├── components │ ├── Button.tsx │ ├── Card.tsx │ ├── CardSlider.tsx │ ├── CardSpace.tsx │ ├── Heading.tsx │ ├── Letter.tsx │ ├── Navigation.tsx │ ├── Offline.tsx │ ├── SplashScreen.tsx │ ├── Tabs.tsx │ ├── TextInput.tsx │ └── index.ts ├── css │ ├── app.css │ ├── base.animations.css │ ├── base.global.css │ ├── base.layout.css │ ├── base.typography.css │ ├── base.utils.css │ ├── component.button.css │ ├── component.card.css │ ├── component.chat.css │ ├── component.heading.css │ ├── component.icons.css │ ├── component.input.css │ ├── component.modal.css │ ├── component.navigation.css │ ├── component.splashScreen.css │ ├── component.tabs.css │ ├── module.app.css │ ├── module.contacts.css │ ├── module.score.css │ ├── module.settings.css │ ├── variables.borders.css │ ├── variables.colors.css │ ├── variables.levels.css │ ├── variables.spacing.css │ ├── variables.typography.css │ └── vendor.toastify.css ├── index.tsx ├── models │ ├── index.ts │ └── models.ts ├── modules │ ├── activities │ │ ├── components │ │ │ ├── ActivityList.tsx │ │ │ ├── ActivityModal.tsx │ │ │ ├── ActivitySuggestion.tsx │ │ │ ├── ActivitySummary.tsx │ │ │ ├── AddActivity.tsx │ │ │ ├── CompletedActivity.tsx │ │ │ └── index.ts │ │ ├── constants │ │ │ ├── constants.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useActivitiesServices.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ └── services │ │ │ ├── addActivity.ts │ │ │ ├── getActivities.ts │ │ │ └── index.ts │ ├── app │ │ ├── components │ │ │ ├── AppLayout.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useAppState.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── models.ts │ │ └── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ ├── assistant │ │ ├── components │ │ │ ├── ChatControls.tsx │ │ │ ├── ChatMessage.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useAssistant.ts │ │ ├── index.ts │ │ └── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ ├── contacts │ │ ├── components │ │ │ ├── AddContact.tsx │ │ │ ├── AddContactModal.tsx │ │ │ ├── ContactCard.tsx │ │ │ ├── ContactGroup.tsx │ │ │ ├── ContactModal.tsx │ │ │ ├── ContactSummary.tsx │ │ │ ├── GroupedContacts.tsx │ │ │ ├── MessageForm.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useContactsServices.ts │ │ │ └── useMessageService.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ └── services │ │ │ ├── addContact.ts │ │ │ ├── getContacts.ts │ │ │ ├── index.ts │ │ │ └── messageContact.ts │ ├── firebase │ │ ├── index.ts │ │ ├── models │ │ │ ├── Collections.ts │ │ │ ├── ListenerProps.ts │ │ │ ├── Ordering.ts │ │ │ ├── QueryFilter.ts │ │ │ └── index.ts │ │ └── services │ │ │ ├── firebaseService.ts │ │ │ ├── firestoreService.ts │ │ │ └── index.ts │ ├── modal │ │ ├── components │ │ │ ├── Modal.tsx │ │ │ ├── Portal.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useModal.ts │ │ │ └── useModalControls.ts │ │ └── index.ts │ ├── redux-store │ │ ├── helpers │ │ │ ├── configureStore.ts │ │ │ ├── createAction.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── models │ │ │ ├── Action.ts │ │ │ ├── ActionUnion.ts │ │ │ ├── ApplicationState.ts │ │ │ └── index.ts │ ├── routing │ │ ├── components │ │ │ ├── PrivateRoute.tsx │ │ │ ├── PublicRoute.tsx │ │ │ ├── Routing.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── score │ │ ├── components │ │ │ ├── ScoreTracker.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useScoreListener.ts │ │ │ └── useScoreServices.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ └── services │ │ │ ├── index.ts │ │ │ └── updateScoreHistory.ts │ ├── settings │ │ ├── components │ │ │ ├── SplashQuestion.tsx │ │ │ ├── SplashSettings.tsx │ │ │ ├── UpdateSettings.tsx │ │ │ └── index.ts │ │ ├── constants │ │ │ ├── index.ts │ │ │ └── questions.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useSettingsServices.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── redux │ │ │ ├── actions.ts │ │ │ ├── index.ts │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── types.ts │ │ └── services │ │ │ ├── index.ts │ │ │ └── updateUserSettings.ts │ └── user │ │ ├── components │ │ ├── LogOut.tsx │ │ ├── LoginWithEmail.tsx │ │ ├── LoginWithGoogle.tsx │ │ ├── SignupEmail.tsx │ │ ├── UpdateProfile.tsx │ │ └── index.ts │ │ ├── hooks │ │ ├── index.ts │ │ ├── useAuthData.ts │ │ ├── useAuthStateChange.ts │ │ └── useUserServices.ts │ │ ├── index.ts │ │ ├── models │ │ ├── index.ts │ │ └── models.ts │ │ ├── redux │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ │ └── services │ │ ├── createUserDocument.ts │ │ └── index.ts ├── react-app-env.d.ts ├── util │ ├── index.ts │ ├── mapping.ts │ ├── time.ts │ └── validation.ts ├── views │ ├── Activities.tsx │ ├── Assistant.tsx │ ├── Contacts.tsx │ ├── Dashboard.tsx │ ├── GeneralError.tsx │ ├── Login.tsx │ ├── NotFound.tsx │ ├── Profile.tsx │ ├── SignUp.tsx │ ├── Welcome.tsx │ └── index.ts └── workers │ ├── index.ts │ └── serviceWorker.ts ├── tsconfig.json └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | # Phone number provided by Twilio messaging service 2 | REACT_APP_TWILIO_PHONE_NUMBER= 3 | 4 | # Twilio trial mode - put authorized phone number here 5 | REACT_APP_TEST_PHONE_NUMBER= 6 | 7 | # Firebase config 8 | REACT_APP_FIREBASE_PROJECT_ID= 9 | REACT_APP_FIREBASE_APP_ID= 10 | REACT_APP_FIREBASE_API_KEY= 11 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID= 12 | REACT_APP_FIREBASE_AUTH_DOMAIN= 13 | REACT_APP_DATABASE_URL= 14 | REACT_APP_FIREBASE_STORAGE_BUCKET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:import/errors", 8 | "plugin:import/warnings", 9 | "plugin:import/typescript", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended" 12 | ], 13 | "plugins": [ 14 | "react", 15 | "import", 16 | "@typescript-eslint", 17 | "react-hooks", 18 | "prettier" 19 | ], 20 | "parserOptions": { 21 | "ecmaVersion": 2018, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "rules": { 28 | "react/prop-types": 0, 29 | "react/self-closing-comp": 1, 30 | "react/no-unescaped-entities": 0, 31 | "react-hooks/rules-of-hooks": "error", 32 | "react-hooks/exhaustive-deps": "warn", 33 | "react/display-name": 0, 34 | "@typescript-eslint/explicit-function-return-type": "off", 35 | "@typescript-eslint/no-use-before-define": [ 36 | "error", 37 | { "functions": false, "classes": true } 38 | ], 39 | "comma-dangle": ["error", "always-multiline"], 40 | "import/order": [ 41 | "error", 42 | { 43 | "newlines-between": "always", 44 | "groups": [ 45 | "builtin", 46 | "external", 47 | "index", 48 | "internal", 49 | "sibling", 50 | "parent" 51 | ] 52 | } 53 | ] 54 | }, 55 | "ignorePatterns": ["./server/*"], 56 | "settings": { 57 | "react": { 58 | "version": "detect" 59 | }, 60 | "import/resolver": { 61 | "node": { 62 | "paths": ["src"] 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "twilio-prototyp" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /server/node_modules 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .env 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # firebase 28 | .firebase/ 29 | .runtimeconfig.json 30 | firebase-debug.log 31 | 32 | # functions 33 | functions/node_modules 34 | functions/lib 35 | .runtimeconfig.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "prettier.jsxBracketSameLine": true, 5 | "trailingComma": "all", 6 | "overrides": [ 7 | { 8 | "files": ["*.tsx", "*.ts", "*.json", "*.yml"], 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-recommended", 3 | "rules": { 4 | "max-nesting-depth": 3, 5 | "property-no-vendor-prefix": true, 6 | "max-empty-lines": 1, 7 | "indentation": 4, 8 | "block-no-empty": null, 9 | "function-calc-no-unspaced-operator": true, 10 | "string-quotes": "single", 11 | "no-duplicate-selectors": true, 12 | "no-missing-end-of-source-newline": true, 13 | "color-hex-case": "lower", 14 | "selector-max-id": 0, 15 | "selector-combinator-space-after": "always", 16 | "declaration-block-trailing-semicolon": "always", 17 | "declaration-colon-space-before": "never", 18 | "declaration-colon-space-after": "always", 19 | "rule-empty-line-before": [ 20 | "always-multi-line", 21 | { 22 | "except": ["after-single-line-comment", "first-nested"] 23 | } 24 | ], 25 | "media-feature-range-operator-space-before": "always", 26 | "media-feature-range-operator-space-after": "always", 27 | "media-feature-parentheses-space-inside": "never", 28 | "media-feature-colon-space-before": "never", 29 | "media-feature-colon-space-after": "always", 30 | "at-rule-no-unknown": [ 31 | true, 32 | { 33 | "ignoreAtRules": ["function", "if", "each", "include", "mixin", "lost"] 34 | } 35 | ], 36 | "property-no-unknown": [ 37 | true, 38 | { 39 | "ignoreProperties": ["/^lost-/"] 40 | } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.validate": ["typescript", "typescriptreact"], 6 | "eslint.enable": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adrian Bece, Vlatko Vlahek, Josip Ravas, Valentina Bermanec, Igor Plac 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 | ## About 2 | 3 | A delightful app for not-so-delightful times that helps people develop healthy routines, keep them informed and keep track of close contacts. Built for DEV x Twilio hackaton 2020. 4 | 5 | - **Activity tracker with gamification elements** - Users earn points by completing tasks (custom or suggested tasks). That way they're encouraged to find new and healthy activities while indoors. 6 | - **Close contacts list** - users can maintain list their close contacts with contact details. They can also use Twilio's programmable SMS to send a message to them in an emergency. 7 | - **Tailored experience dependant on the situation** - Recommend different activities for users based on the following criteria: if they are in a mandatory self-isolation or not and if they live alone or not. 8 | - **Chatbot assistance** - Users can get informed about self-isolation rules. Currently with very basic chatbot flow, but with wide possibilities for extending it, depending on the config in the Twilio console. 9 | 10 | ## Installation 11 | 12 | ### Pre-requisites 13 | 14 | - Firebase Firestore (with Blaze account) 15 | - Twilio account (trial or fully activated) 16 | 17 | ### Project setup 18 | 19 | Clone the repository (develop or master branch) 20 | 21 | Run `yarn install` on cloned project on a local machine. 22 | 23 | Create `.env` in project root based on `.env.sample` file. Add Firebase API keys, Twilio phone number there and test phone number (for trial account). 24 | 25 | Deploy Twilio api keys to firebase 26 | 27 | ``` 28 | node_modules/.bin/firebase functions:config:set twilio.sid="..." twilio.token="..." twilio.twilioapikey="..." twilio.twilioapisecret="..." 29 | ``` 30 | 31 | Run the project with 32 | 33 | ``` 34 | yarn start 35 | ``` 36 | 37 | Build and deploy project to production 38 | 39 | ``` 40 | yarn build && firebase deploy 41 | ``` 42 | 43 | ## License 44 | 45 | [MIT](http://www.opensource.org/licenses/mit-license.html) 46 | 47 | ## Disclaimer 48 | 49 | No warranty expressed or implied. Software is as is. 50 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | require('react-app-rewire-postcss')(config, { 3 | plugins: (loader) => [ 4 | require('postcss-import')(), 5 | require('postcss-mixins')(), 6 | require('postcss-preset-env')({ 7 | autoprefixer: { 8 | flexbox: 'no-2009', 9 | }, 10 | features: { 11 | 'nesting-rules': true, 12 | 'custom-media-queries': true, 13 | }, 14 | stage: 3, 15 | }), 16 | require('cssnano')(), 17 | ], 18 | }); 19 | 20 | return config; 21 | }; 22 | -------------------------------------------------------------------------------- /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 | "headers": [ 16 | { 17 | "source": "**/*", 18 | "headers": [ 19 | { 20 | "key": "X-Frame-Options", 21 | "value": "deny" 22 | } 23 | ] 24 | }, 25 | { 26 | "source": "**/*.@(eot|otf|ttf|ttc|woff|woff2|font.css)", 27 | "headers": [ 28 | { 29 | "key": "Access-Control-Allow-Origin", 30 | "value": "*" 31 | }, 32 | { 33 | "key": "Cache-Control", 34 | "value": "max-age=31536000" 35 | } 36 | ] 37 | }, 38 | { 39 | "source": "**/*.@(jpg|jpeg|gif|png|svg)", 40 | "headers": [ 41 | { 42 | "key": "Cache-Control", 43 | "value": "max-age=31536000" 44 | } 45 | ] 46 | }, 47 | { 48 | "source": "**/*.@(js|css)", 49 | "headers": [ 50 | { 51 | "key": "Cache-Control", 52 | "value": "max-age=31536000" 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | "functions": { 59 | "predeploy": [ 60 | "npm --prefix \"$RESOURCE_DIR\" run lint", 61 | "npm --prefix \"$RESOURCE_DIR\" run build" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /activities/{userId}/{document=**} { 5 | allow read, update, delete: if request.auth.uid == userId; 6 | allow create: if request.auth.uid != null; 7 | } 8 | match /contacts/{userId}/{document=**} { 9 | allow read, update, delete: if request.auth.uid == userId; 10 | allow create: if request.auth.uid != null; 11 | } 12 | match /score/{userId} { 13 | allow read, update, delete: if request.auth.uid == userId; 14 | allow create: if request.auth.uid != null; 15 | } 16 | match /score/{userId}/{document=**} { 17 | allow read, update, delete: if request.auth.uid == userId; 18 | allow create: if request.auth.uid != null; 19 | } 20 | match /settings/{userId} { 21 | allow read, update, delete: if request.auth.uid == userId; 22 | allow create: if request.auth.uid != null; 23 | } 24 | match /users/{userId} { 25 | allow read, update, delete: if request.auth.uid == userId; 26 | allow create: if request.auth.uid != null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build-watch": "tsc -w", 6 | "build": "tsc", 7 | "serve": "npm run build-watch && firebase emulators:start --only functions", 8 | "shell": "npm run build && firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": "8" 15 | }, 16 | "main": "lib/index.js", 17 | "dependencies": { 18 | "firebase-admin": "^8.9.0", 19 | "firebase-functions": "^3.3.0", 20 | "twilio": "3.42.1" 21 | }, 22 | "devDependencies": { 23 | "tslint": "^5.12.0", 24 | "typescript": "^3.2.2", 25 | "firebase-functions-test": "^0.1.6", 26 | "firebase-tools": "8.0.2" 27 | }, 28 | "private": true 29 | } 30 | -------------------------------------------------------------------------------- /functions/src/createUserChannel.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import { Twilio } from 'twilio'; 3 | 4 | export const createUserChannel = functions.https.onCall(async (data) => { 5 | const { sid, token } = functions.config().twilio; 6 | const client = new Twilio(sid, token); 7 | 8 | const services = await client.chat.services.list(); 9 | const coronaChat = services.find((s) => s.friendlyName === 'CoronaBot'); 10 | const { userId } = data; 11 | 12 | if (!coronaChat) { 13 | throw new Error('Chat service is not found'); 14 | } 15 | 16 | try { 17 | const channel = await client.chat 18 | .services(coronaChat.sid) 19 | .channels.create({ friendlyName: userId, uniqueName: userId }); 20 | 21 | await client.chat 22 | .services(coronaChat.sid) 23 | .channels(channel.sid) 24 | .webhooks.create({ 25 | type: 'webhook', 26 | configuration: { 27 | filters: ['onMessageSent'], 28 | method: 'POST', 29 | url: 30 | 'https://channels.autopilot.twilio.com/v1/ACb57077d04b3c7e514a7f26f8cfc9c28a/UA5d7e5d90dde8b6c82e8395bc5eaa9425/twilio-chat', 31 | }, 32 | }); 33 | 34 | return channel.sid; 35 | } catch (e) { 36 | const channelList = await client.chat 37 | .services(coronaChat.sid) 38 | .channels.list(); 39 | 40 | const channel = channelList.find((chn) => chn.uniqueName === userId); 41 | return channel?.sid; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /functions/src/getUserToken.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import { jwt, Twilio } from 'twilio'; 3 | 4 | const AccessToken = jwt.AccessToken; 5 | const ChatGrant = AccessToken.ChatGrant; 6 | 7 | export const getUserToken = functions.https.onCall(async (data) => { 8 | const { 9 | sid, 10 | token, 11 | twilioapikey, 12 | twilioapisecret, 13 | } = functions.config().twilio; 14 | 15 | const client = new Twilio(sid, token); 16 | const services = await client.chat.services.list(); 17 | const coronaChat = services.find((s) => s.friendlyName === 'CoronaBot'); 18 | 19 | if (!coronaChat) { 20 | throw new Error('Chat service is not found'); 21 | } 22 | const serviceSid = coronaChat.sid; 23 | 24 | const { identity } = data; 25 | 26 | // Create a "grant" which enables a client to use Chat as a given user, 27 | // on a given device 28 | const chatGrant = new ChatGrant({ 29 | serviceSid: serviceSid, 30 | }); 31 | 32 | // Create an access token which we will sign and return to the client, 33 | // containing the grant we just created 34 | const chatToken = new AccessToken(sid, twilioapikey, twilioapisecret, { 35 | identity, 36 | }); 37 | 38 | chatToken.addGrant(chatGrant); 39 | 40 | // Serialize the token to a JWT string 41 | return chatToken.toJwt(); 42 | }); 43 | -------------------------------------------------------------------------------- /functions/src/incrementUserScore.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import admin = require('firebase-admin'); 3 | 4 | const firestore = admin.firestore(); 5 | 6 | export const incrementUserScore = functions.firestore 7 | .document('score/{userId}/history/{scoreId}') 8 | .onCreate(async (_, context) => { 9 | const { userId, scoreId } = context.params; 10 | const scoreRef = firestore.doc(`score/${userId}`); 11 | const historyRef = firestore.doc(`score/${userId}/history/${scoreId}`); 12 | 13 | const scoreSnap = await scoreRef.get(); 14 | const historySnap = await historyRef.get(); 15 | 16 | const scoreCurrent = scoreSnap.get('score'); 17 | const scoreIncrement = historySnap.get(`score`); 18 | 19 | return scoreRef.update({ score: scoreCurrent + scoreIncrement }); 20 | }); 21 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import admin = require('firebase-admin'); 2 | 3 | admin.initializeApp(); 4 | 5 | export * from './incrementUserScore'; 6 | export * from './sendSMSMessage'; 7 | export * from './createUserChannel'; 8 | export * from './removeUserChannel'; 9 | export * from './getUserToken'; 10 | -------------------------------------------------------------------------------- /functions/src/removeUserChannel.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import { Twilio } from 'twilio'; 3 | 4 | export const removeUserChannel = functions.https.onCall(async (data) => { 5 | const { sid, token } = functions.config().twilio; 6 | const client = new Twilio(sid, token); 7 | 8 | const services = await client.chat.services.list(); 9 | const coronaChat = services.find((s) => s.friendlyName === 'CoronaBot'); 10 | 11 | if (!coronaChat) { 12 | throw new Error('Chat service is not found'); 13 | } 14 | 15 | try { 16 | const { channelId } = data; 17 | const channel = await client.chat 18 | .services(coronaChat.sid) 19 | .channels(channelId) 20 | .fetch(); 21 | 22 | return await channel.remove(); 23 | } catch (e) { 24 | console.log(e); 25 | return null; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /functions/src/sendSMSMessage.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import { Twilio } from 'twilio'; 3 | 4 | export const sendSMSMessage = functions.https.onCall(async (data) => { 5 | const { sid, token } = functions.config().twilio; 6 | const client = new Twilio(sid, token); 7 | 8 | const { from, to, body } = data; 9 | return client.messages 10 | .create({ 11 | from, 12 | to, 13 | body, 14 | }) 15 | .then(() => ({ success: true })) 16 | .catch((err: string) => { 17 | console.log(err); 18 | return { success: false }; 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | interface Navigator { 2 | connection: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebound", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": { 6 | "name": "Adrian Bece", 7 | "email": "adrian@prototyp.digital", 8 | "url": "https://github.com/codeadrian" 9 | }, 10 | "dependencies": { 11 | "date-fns": "^2.12.0", 12 | "firebase": "^7.13.2", 13 | "lodash": "^4.17.15", 14 | "normalize.css": "^8.0.1", 15 | "react": "^16.13.1", 16 | "react-circular-progressbar": "^2.0.3", 17 | "react-dom": "^16.13.1", 18 | "react-hook-form": "^5.4.2", 19 | "react-redux": "^7.2.0", 20 | "react-router-dom": "^5.1.2", 21 | "react-toastify": "^5.5.0", 22 | "react-use-is-online": "^1.0.3", 23 | "redux": "^4.0.5", 24 | "twilio-chat": "^3.3.5" 25 | }, 26 | "scripts": { 27 | "start": "react-app-rewired start", 28 | "build": "react-app-rewired build", 29 | "lint": "eslint src --ext .ts,.tsx", 30 | "lint:fix": "eslint --fix src --ext .ts,.tsx" 31 | }, 32 | "license": "MIT", 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/lodash": "^4.14.149", 50 | "@types/node": "^12.0.0", 51 | "@types/react": "^16.9.0", 52 | "@types/react-dom": "^16.9.0", 53 | "@types/react-redux": "^7.1.7", 54 | "@types/react-router-dom": "^5.1.3", 55 | "body-parser": "^1.19.0", 56 | "cssnano": "^4.1.10", 57 | "eslint-config-prettier": "^6.10.1", 58 | "eslint-plugin-import": "^2.20.2", 59 | "eslint-plugin-prettier": "^3.1.2", 60 | "eslint-plugin-react-hooks": "^3.0.0", 61 | "firebase-tools": "^8.0.3", 62 | "postcss-import": "^12.0.1", 63 | "postcss-mixins": "^6.2.3", 64 | "postcss-preset-env": "^6.7.0", 65 | "prettier": "^2.0.4", 66 | "react-app-rewire-postcss": "^3.0.2", 67 | "react-app-rewired": "^2.1.5", 68 | "react-scripts": "3.4.1", 69 | "redux-devtools-extension": "^2.13.8", 70 | "stylelint": "^13.3.1", 71 | "stylelint-config-recommended": "^3.0.0", 72 | "typescript": "~3.7.2" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #fac936 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 16 | 22 | 28 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | HomeBound 43 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "HomeBound", 3 | "name": "HomeBound", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#fac936", 24 | "background_color": "#fac936" 25 | } 26 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeAdrian/homeBound/fbc70d3b52773c396f7901244334e095806c38bf/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HomeBound", 3 | "short_name": "HomeBound", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#fac936", 17 | "background_color": "#fac936", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import { configureStore } from 'modules/redux-store'; 6 | import 'normalize.css'; 7 | import 'css/app.css'; 8 | import { AppLayout } from 'modules/app'; 9 | 10 | const App: React.FC = () => { 11 | const store = configureStore(); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/chevron_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/circle_ring.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/circle_star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/plus_large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/speech_bubble.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/squares.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/text_bubble.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/trophy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface OwnProps { 4 | className?: string; 5 | icon?: React.ReactNode; 6 | value?: string; 7 | } 8 | 9 | type Props = OwnProps & React.HTMLAttributes; 10 | 11 | export const BUTTON = { 12 | SQUARE: { 13 | LARGE: { 14 | CTA: 15 | 'button button--square-large button--size-xxlarge button--block u-t__fontSize--large u-t__fontWeight--light button--square-cta', 16 | PRIMARY: 17 | 'button button--square-large button--size-xxlarge button--block u-t__fontSize--large u-t__fontWeight--light button--square-primary', 18 | }, 19 | GHOST: { 20 | MIXED: 'button button--ghost button--size-mixed button--square', 21 | }, 22 | CTA: { 23 | MIXED: { 24 | GLOW: 25 | 'button button--cta button--glow-cta button--size-mixed button--square', 26 | }, 27 | }, 28 | }, 29 | PILL: { 30 | PRIMARY: { 31 | BASE: 32 | 'button button--primary button--block button--pill button--size-base u-t__fontSize--small', 33 | }, 34 | SECONDARY: { 35 | DEFAULT: 36 | 'button button--secondary button--block button--pill--small button--size-small u-t__fontSize--small', 37 | ACTIVE: 38 | 'button button--secondary button--secondary--active button--block button--pill--small button--size-small u-t__fontSize--small', 39 | }, 40 | CTA: { 41 | BASE: { 42 | GLOW: 43 | 'button button--cta button--glow-cta button--block button--pill button--size-base u-t__fontSize--small', 44 | }, 45 | }, 46 | }, 47 | COMBINED: { 48 | PRIMARY: { 49 | BASE: { 50 | TOP: 51 | 'button button--primary button--combined button--block button--combined button--top button--size-xlarge u-t__fontSize--small', 52 | BOTTOM: 53 | 'button button--primary button--combined button--block button--combined button--bottom button--size-xlarge u-t__fontSize--small', 54 | }, 55 | }, 56 | }, 57 | ROUNDED: { 58 | CTA: { 59 | SMALL: 60 | 'button button--cta button--rounded button--size-xxsmall u-t__fontSize--small', 61 | LARGE: { 62 | GLOW: 63 | 'button button--cta button--rounded button--glow-cta button--size-small u-t__fontSize--small', 64 | }, 65 | }, 66 | }, 67 | }; 68 | 69 | export const Button: React.FC = ({ children, icon, ...rest }) => { 70 | return ( 71 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/CardSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CardSlider: React.FC = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/CardSpace.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CardSpace = () => { 4 | return
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div'; 5 | className: string; 6 | } 7 | 8 | export const HEADING = { 9 | PRIMARY: { 10 | XSMALL: { 11 | MEDIUM: 12 | 'heading u-t__fontFamily--primary u-t__fontSize--xsmall u-t__fontWeight--medium', 13 | }, 14 | XLARGE: { 15 | BOLD: 16 | 'heading u-t__fontFamily--primary u-t__fontSize--xlarge u-t__fontWeight--bold', 17 | }, 18 | 19 | XXLARGE: { 20 | LIGHT: 21 | 'heading u-t__fontFamily--primary u-t__fontSize--xxlarge u-t__fontWeight--light', 22 | }, 23 | }, 24 | SECONDARY: { 25 | SMALL: { 26 | BOLD: 27 | 'heading u-t__fontFamily--secondary u-t__fontSize--small u-t__fontWeight--bold', 28 | }, 29 | XLARGE: { 30 | BOLD: 31 | 'heading u-t__fontFamily--secondary u-t__fontSize--xlarge u-t__fontWeight--bold', 32 | }, 33 | }, 34 | }; 35 | 36 | export const Heading: React.FC = ({ children, tag: Tag, ...rest }) => { 37 | return {children}; 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Letter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Letter: React.FC = ({ children }) => { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { ReactComponent as PersonIcon } from 'assets/icons/person.svg'; 5 | import { ReactComponent as DashboardIcon } from 'assets/icons/squares.svg'; 6 | import { ReactComponent as ActivitiesIcon } from 'assets/icons/trophy.svg'; 7 | import { ReactComponent as SpeechBubbleIcon } from 'assets/icons/speech_bubble.svg'; 8 | import { useAppState } from 'modules/app'; 9 | 10 | export const Navigation = () => { 11 | const [{ theme }] = useAppState(); 12 | 13 | const navClassName = theme.showNav ? 'nav' : 'nav nav--hidden'; 14 | const navTheme = theme.color === '#6A62FF' ? 'app--light' : ''; 15 | 16 | return ( 17 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/Offline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Heading, HEADING } from 'components'; 4 | import { useAppState } from 'modules/app'; 5 | 6 | export const Offline = () => { 7 | const [ 8 | { 9 | theme: { showNav }, 10 | }, 11 | { setAppTheme }, 12 | ] = useAppState(); 13 | 14 | React.useEffect(() => { 15 | setAppTheme({ 16 | color: '#6A62FF', 17 | shapeClass: 'app__deco--default', 18 | showNav, 19 | }); 20 | }, [setAppTheme, showNav]); 21 | 22 | return ( 23 |
24 | 29 |
30 |
31 | 32 | It seems that you're offline. 33 | 34 |
35 | 36 | Thats okay, too. Use this time to have some rest inbetween having fun 37 | and doing things. 38 | 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/SplashScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ReactComponent as Logo } from 'assets/icons/logo.svg'; 4 | import { useAppState } from 'modules/app'; 5 | 6 | export const SplashScreen = () => { 7 | const [, { setAppTheme }] = useAppState(); 8 | 9 | React.useEffect(() => { 10 | setAppTheme({ 11 | color: '#FAC936', 12 | shapeClass: 'app__deco--default', 13 | showNav: false, 14 | }); 15 | }, [setAppTheme]); 16 | 17 | return ( 18 |
19 | 20 |
21 | 22 | HomeBound 23 | 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | contentMain: React.ReactNode; 5 | contentSecondary: React.ReactNode; 6 | titleMain: string; 7 | titleSecondary: string; 8 | mode?: 'cta'; 9 | } 10 | 11 | export const Tabs: React.FC = ({ 12 | contentMain, 13 | contentSecondary, 14 | titleMain, 15 | titleSecondary, 16 | mode, 17 | }) => { 18 | const [mainTabActive, setMainTabActive] = React.useState(true); 19 | 20 | const buttonMainClassName = mainTabActive ? 'tabs__button--active' : ''; 21 | const buttonSecondaryClassName = !mainTabActive ? 'tabs__button--active' : ''; 22 | 23 | return ( 24 | <> 25 | 43 | {mainTabActive ? contentMain : contentSecondary} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NestDataObject, FieldError } from 'react-hook-form'; 3 | 4 | interface OwnProps { 5 | name: string; 6 | label: string; 7 | hasValue?: boolean; 8 | errors?: NestDataObject, FieldError>; 9 | componentRef: ( 10 | ref: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null, 11 | ) => void; 12 | } 13 | 14 | type Props = OwnProps & React.HTMLProps; 15 | 16 | export const TextInput: React.FC = ({ 17 | label, 18 | componentRef, 19 | name, 20 | className, 21 | errors, 22 | hasValue, 23 | ...props 24 | }) => { 25 | const [isFocused, setIsFocused] = React.useState(false); 26 | 27 | const handleFocus = () => { 28 | setIsFocused(true); 29 | }; 30 | const handleBlur = () => { 31 | setIsFocused(false); 32 | }; 33 | 34 | const error = errors && errors[name]; 35 | 36 | const shouldTransformLabel = isFocused || hasValue; 37 | 38 | const inputClassName = shouldTransformLabel 39 | ? 'input input--focused' 40 | : 'input'; 41 | 42 | const labelClassName = shouldTransformLabel 43 | ? 'input__label u-t__fontSize--base input__label--focused' 44 | : 'input__label u-t__fontSize--base'; 45 | 46 | return ( 47 |
48 | 51 | 60 | 61 | {error && ( 62 |
63 | {error.message} 64 |
65 | )} 66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | export * from './TextInput'; 3 | export * from './SplashScreen'; 4 | export * from './Heading'; 5 | export * from './Card'; 6 | export * from './CardSpace'; 7 | export * from './CardSlider'; 8 | export * from './Tabs'; 9 | export * from './Letter'; 10 | export * from './Offline'; 11 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'base.global.css'; 2 | 3 | @import 'variables.colors.css'; 4 | @import 'variables.spacing.css'; 5 | @import 'variables.typography.css'; 6 | @import 'variables.borders.css'; 7 | @import 'variables.levels.css'; 8 | 9 | @import 'base.animations.css'; 10 | @import 'base.typography.css'; 11 | @import 'base.utils.css'; 12 | @import 'base.layout.css'; 13 | 14 | @import 'component.button.css'; 15 | @import 'component.heading.css'; 16 | @import 'component.input.css'; 17 | @import 'component.splashScreen.css'; 18 | @import 'component.icons.css'; 19 | @import 'component.navigation.css'; 20 | @import 'component.modal.css'; 21 | @import 'component.tabs.css'; 22 | @import 'component.card.css'; 23 | @import 'component.chat.css'; 24 | 25 | @import 'module.app.css'; 26 | @import 'module.score.css'; 27 | @import 'module.contacts.css'; 28 | @import 'module.settings.css'; 29 | 30 | @import 'vendor.toastify.css'; 31 | -------------------------------------------------------------------------------- /src/css/base.animations.css: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn { 2 | from { 3 | transform: translate3d(0, var(--spacing--6px), 0); 4 | opacity: 0; 5 | } 6 | 7 | to { 8 | transform: translate3d(0, 0, 0); 9 | opacity: 1; 10 | } 11 | } 12 | 13 | @keyframes slideInLeft { 14 | from { 15 | transform: translate3d(-100%, 0, 0); 16 | } 17 | 18 | to { 19 | transform: translate3d(28px, 0, 0); 20 | } 21 | } 22 | 23 | @keyframes slideInRight { 24 | from { 25 | transform: translate3d(0, 0, 0); 26 | } 27 | 28 | to { 29 | transform: translate3d(-100%, 0, 0); 30 | } 31 | } 32 | 33 | @keyframes jump { 34 | from { 35 | transform: scale(3) translate3d(0, 0, 0); 36 | } 37 | to { 38 | transform: scale(3) translate3d(0, var(--spacing--6px), 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/css/base.global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | min-height: 100vh; 4 | margin: 0; 5 | } 6 | 7 | body { 8 | scroll-behavior: smooth; 9 | display: flex; 10 | justify-content: space-around; 11 | align-items: center; 12 | background-color: #e5e5e5; 13 | } 14 | 15 | html { 16 | box-sizing: border-box; 17 | } 18 | 19 | *, 20 | *:before, 21 | *:after { 22 | box-sizing: inherit; 23 | } 24 | 25 | img, 26 | svg { 27 | display: block; 28 | max-width: 100%; 29 | height: auto; 30 | } 31 | 32 | .root { 33 | border-radius: var(--border__radius--20px); 34 | overflow: hidden; 35 | position: relative; 36 | height: 568px; 37 | width: 320px; 38 | } 39 | 40 | ::-webkit-scrollbar { 41 | width: 8px; 42 | } 43 | 44 | ::-webkit-scrollbar * { 45 | background: transparent; 46 | } 47 | 48 | ::-webkit-scrollbar-thumb { 49 | background: rgba(0, 0, 0, 0.3) !important; 50 | border-radius: 4px; 51 | } 52 | -------------------------------------------------------------------------------- /src/css/base.layout.css: -------------------------------------------------------------------------------- 1 | .l-vertical { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | } 6 | 7 | .l-page { 8 | display: flex; 9 | flex-direction: column; 10 | height: 100%; 11 | 12 | & main { 13 | flex-grow: 1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/css/base.typography.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: var(--font__family--primary); 3 | font-size: var(--font__size--base); 4 | line-height: var(--font__line--base); 5 | -webkit-font-smoothing: antialiased; 6 | } 7 | 8 | .u-t__fontFamily--secondary { 9 | font-family: var(--font__family--secondary); 10 | } 11 | 12 | .u-t__fontSize--xxsmall { 13 | font-size: var(--font__size--xxsmall); 14 | line-height: var(--font__line--xxsmall); 15 | } 16 | 17 | .u-t__fontSize--xsmall { 18 | font-size: var(--font__size--xsmall); 19 | line-height: var(--font__line--xsmall); 20 | } 21 | 22 | .u-t__fontSize--small { 23 | font-size: var(--font__size--small); 24 | line-height: var(--font__line--small); 25 | } 26 | 27 | .u-t__fontSize--base { 28 | font-size: var(--font__size--base); 29 | line-height: var(--font__line--base); 30 | } 31 | 32 | .u-t__fontSize--large { 33 | font-size: var(--font__size--large); 34 | line-height: var(--font__line--large); 35 | } 36 | 37 | .u-t__fontSize--xlarge { 38 | font-size: var(--font__size--xlarge); 39 | line-height: var(--font__line--xlarge); 40 | } 41 | 42 | .u-t__fontSize--xxlarge { 43 | font-size: var(--font__size--xxlarge); 44 | line-height: var(--font__line--xxlarge); 45 | } 46 | 47 | .u-t__fontWeight--light { 48 | font-weight: var(--font__weight--light); 49 | } 50 | 51 | .u-t__fontWeight--regular { 52 | font-weight: var(--font__weight--regular); 53 | } 54 | 55 | .u-t__fontWeight--medium { 56 | font-weight: var(--font__weight--medium); 57 | } 58 | 59 | .u-t__fontWeight--bold { 60 | font-weight: var(--font__weight--bold); 61 | } 62 | 63 | strong { 64 | font-weight: var(--font__weight--bold); 65 | } 66 | 67 | p { 68 | margin-top: 0; 69 | margin-bottom: var(--spacing--16px); 70 | } 71 | 72 | a { 73 | text-decoration: none; 74 | 75 | &, 76 | &:visited { 77 | color: currentColor; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/css/base.utils.css: -------------------------------------------------------------------------------- 1 | .u-t-center { 2 | text-align: center; 3 | } 4 | 5 | .u-o-8 { 6 | opacity: 0.8; 7 | } 8 | 9 | .u-o-6 { 10 | opacity: 0.6; 11 | } 12 | 13 | .u-o-5 { 14 | opacity: 0.5; 15 | } 16 | 17 | .u-o-4 { 18 | opacity: 0.4; 19 | } 20 | 21 | .u-o-1 { 22 | opacity: 0.1; 23 | } 24 | 25 | .u-c-dark { 26 | color: var(--color__text); 27 | } 28 | 29 | .u-c-light { 30 | color: var(--color__white); 31 | } 32 | 33 | .u-sb-40 { 34 | margin-bottom: var(--spacing--40px); 35 | } 36 | 37 | .u-sb-28 { 38 | margin-bottom: var(--spacing--28px); 39 | } 40 | 41 | .u-sb-36 { 42 | margin-bottom: var(--spacing--36px); 43 | } 44 | 45 | .u-sb-12 { 46 | margin-bottom: var(--spacing--12px); 47 | } 48 | 49 | .u-sb-16 { 50 | margin-bottom: var(--spacing--16px); 51 | } 52 | 53 | .u-sb-8 { 54 | margin-bottom: var(--spacing--8px); 55 | } 56 | 57 | .u-sr-32 { 58 | padding-right: var(--spacing--32px); 59 | } 60 | 61 | .u-ab-center { 62 | margin-left: auto; 63 | margin-right: auto; 64 | } 65 | 66 | .u-d-block { 67 | display: block; 68 | } 69 | 70 | .u-c-cta { 71 | color: var(--color__cta); 72 | } 73 | 74 | .u-l-6 { 75 | position: relative; 76 | z-index: var(--level-6); 77 | } 78 | 79 | .u-t-truncate { 80 | text-overflow: ellipsis; 81 | white-space: nowrap; 82 | overflow: hidden; 83 | } 84 | 85 | .u-f--spaceBetween { 86 | display: flex; 87 | justify-content: space-between; 88 | align-items: center; 89 | } 90 | 91 | .u-f--spaceBetween--top { 92 | align-items: flex-start; 93 | } 94 | 95 | .u-f--grow1 { 96 | flex-grow: 1; 97 | } 98 | -------------------------------------------------------------------------------- /src/css/component.chat.css: -------------------------------------------------------------------------------- 1 | .chat__message { 2 | position: relative; 3 | width: 236px; 4 | color: var(--color__text); 5 | padding: var(--spacing--12px) var(--spacing--16px); 6 | background-color: var(--color__white); 7 | border-radius: var(--border__radius--12px); 8 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); 9 | margin-bottom: var(--spacing--28px); 10 | } 11 | 12 | .chat__controls { 13 | position: relative; 14 | max-width: 196px; 15 | } 16 | 17 | .chat__message--animated { 18 | animation-direction: normal; 19 | animation-fill-mode: forwards; 20 | animation-duration: 0.4s; 21 | animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); 22 | } 23 | 24 | .chat__message--bot { 25 | left: 0; 26 | animation-name: slideInLeft; 27 | transform: translate3d(-100%, 0, 0); 28 | } 29 | 30 | .chat__message--user { 31 | left: calc(100% - var(--spacing--28px)); 32 | animation-name: slideInRight; 33 | transform: translate3d(28px, 0, 0); 34 | } 35 | 36 | .chat__wrapper { 37 | margin-left: -40px; 38 | margin-right: -40px; 39 | position: relative; 40 | overflow-x: hidden; 41 | min-height: 100%; 42 | } 43 | 44 | .chat__loadingWrapper { 45 | padding-top: var(--spacing--120px); 46 | margin-bottom: var(--spacing--20px); 47 | padding-bottom: var(--spacing--20px); 48 | } 49 | 50 | .chat__loading { 51 | display: flex; 52 | flex-direction: column; 53 | justify-content: space-around; 54 | padding-bottom: var(--spacing--120px); 55 | } 56 | 57 | .chat__icon { 58 | transform: scale(3) translate3d(0, 0, 0); 59 | } 60 | 61 | .chat__icon--animated { 62 | animation: jump 1s alternate forwards infinite ease; 63 | } 64 | 65 | .chat__text { 66 | text-overflow: ellipsis; 67 | overflow: hidden; 68 | display: block; 69 | 70 | a { 71 | text-decoration: underline; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/css/component.heading.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | margin: 0; 3 | color: currentColor; 4 | } 5 | 6 | .heading--secondary { 7 | font-family: var(--font__family--primary); 8 | } 9 | 10 | .heading--bold { 11 | font-weight: var(--font__weight--bold); 12 | } 13 | 14 | .headingBanner { 15 | display: inline-block; 16 | background-color: var(--color__white); 17 | color: var(--color__text); 18 | padding: var(--spacing--2px) var(--spacing--8px); 19 | 20 | & .heading { 21 | margin: 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/css/component.icons.css: -------------------------------------------------------------------------------- 1 | .icon--small { 2 | width: var(--spacing--12px); 3 | height: var(--spacing--12px); 4 | } 5 | 6 | .icon--lightest { 7 | fill: var(--color__white); 8 | } 9 | 10 | .icon--darker { 11 | fill: var(--color__text); 12 | } 13 | -------------------------------------------------------------------------------- /src/css/component.modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: absolute; 3 | height: 100%; 4 | min-width: 100%; 5 | top: 0; 6 | padding: var(--spacing--32px) var(--spacing--40px) var(--spacing--28px); 7 | left: 0; 8 | right: 0; 9 | z-index: var(--level-5); 10 | opacity: 0; 11 | pointer-events: none; 12 | transition: opacity 0.5s ease; 13 | border-radius: var(--border__radius--20px); 14 | overflow: auto; 15 | } 16 | 17 | .modal--open { 18 | opacity: 1; 19 | pointer-events: all; 20 | } 21 | -------------------------------------------------------------------------------- /src/css/component.navigation.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | position: absolute; 3 | bottom: 0; 4 | left: 0; 5 | right: 0; 6 | z-index: var(--level-3); 7 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.04); 8 | border-radius: 20px; 9 | padding: 20px; 10 | display: flex; 11 | justify-content: space-around; 12 | opacity: 1; 13 | transition: opacity 0.8s 0.4s ease; 14 | } 15 | 16 | .nav--hidden { 17 | opacity: 0; 18 | pointer-events: none; 19 | transition: opacity 0.2s ease; 20 | } 21 | 22 | .nav__background { 23 | content: ''; 24 | z-index: var(--level-2); 25 | position: absolute; 26 | left: 0; 27 | right: 0; 28 | top: 0; 29 | bottom: 0; 30 | opacity: 0.9; 31 | border-radius: 20px; 32 | backdrop-filter: blur(60px); 33 | transition: background-color 0.8s ease; 34 | } 35 | 36 | .nav__link { 37 | position: relative; 38 | z-index: var(--level-4); 39 | fill: var(--color__white); 40 | outline: 0; 41 | border-width: 0; 42 | display: inline-flex; 43 | justify-content: center; 44 | align-items: center; 45 | cursor: pointer; 46 | transition: background-color 0.3s ease, opacity 0.3s ease, color 0.3s ease; 47 | border-radius: var(--border__radius--10px); 48 | background-color: transparent; 49 | padding: var(--spacing--10px) var(--spacing--12px); 50 | opacity: 0.3; 51 | width: 40px; 52 | height: 40px; 53 | 54 | &, 55 | &:visited { 56 | color: currentColor; 57 | } 58 | } 59 | 60 | .nav__icon { 61 | &, 62 | & * { 63 | fill: currentColor; 64 | } 65 | } 66 | 67 | .nav__link--active { 68 | opacity: 1; 69 | background-color: var(--color__cta); 70 | box-shadow: 0px var(--spacing--8px) var(--spacing--16px) 71 | var(--color__glow--cta); 72 | 73 | &, 74 | &:visited { 75 | color: var(--color__white); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/css/component.splashScreen.css: -------------------------------------------------------------------------------- 1 | .splashScreen { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | text-align: center; 6 | height: 100%; 7 | position: absolute !important; 8 | left: 0; 9 | right: 0; 10 | } 11 | 12 | .splashScreen__logo { 13 | margin: 0 auto var(--spacing--16px); 14 | } 15 | 16 | .splashScreen__title { 17 | color: var(--color__text); 18 | } 19 | -------------------------------------------------------------------------------- /src/css/component.tabs.css: -------------------------------------------------------------------------------- 1 | .tabs__nav--cta { 2 | color: var(--color__cta); 3 | } 4 | 5 | .tabs__nav { 6 | display: flex; 7 | margin-left: var(--spacing--n40px); 8 | margin-right: var(--spacing--n40px); 9 | padding: 0 var(--spacing--40px); 10 | justify-content: flex-start; 11 | position: relative; 12 | 13 | &::after { 14 | content: ''; 15 | position: absolute; 16 | left: 0; 17 | right: 0; 18 | height: 1px; 19 | width: 100%; 20 | bottom: -1px; 21 | opacity: 0.1; 22 | background-color: currentColor; 23 | } 24 | } 25 | 26 | .tabs__button { 27 | transition: box-shadow 0.25s ease; 28 | margin-right: var(--spacing--24px); 29 | padding-bottom: var(--spacing--12px); 30 | box-shadow: inset 0 0 0 0 currentColor; 31 | 32 | & span { 33 | transition: opacity 0.25s ease; 34 | opacity: 0.4; 35 | } 36 | } 37 | 38 | .tabs__button--active { 39 | box-shadow: inset 0 -2px 0 0 currentColor; 40 | 41 | & span { 42 | opacity: 1; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/css/module.app.css: -------------------------------------------------------------------------------- 1 | .app { 2 | color: var(--color__text); 3 | } 4 | 5 | .app--light { 6 | color: var(--color__white); 7 | } 8 | 9 | .app__background { 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | transition: background-color 0.8s ease; 14 | border-radius: var(--border__radius--20px); 15 | overflow: auto; 16 | scrollbar-color: rgb(0, 0, 0); 17 | scrollbar-width: thin; 18 | } 19 | 20 | .app__content { 21 | overflow-x: visible; 22 | position: relative; 23 | z-index: var(--level-1); 24 | opacity: 0; 25 | padding: var(--spacing--32px) var(--spacing--40px) 0; 26 | animation: fadeIn 0.8s 0.1s ease forwards; 27 | flex: 1; 28 | } 29 | 30 | .app__background--withNav { 31 | & .app__content { 32 | flex: 0; 33 | padding: var(--spacing--32px) var(--spacing--40px) var(--spacing--80px); 34 | } 35 | } 36 | 37 | .app__background--primary { 38 | background-color: var(--color__background--primary); 39 | } 40 | 41 | .app__background--secondary { 42 | background-color: var(--color__background--secondary); 43 | } 44 | 45 | .app__background--tertiary { 46 | background-color: var(--color__background--tertiary); 47 | } 48 | 49 | .app__background--quaternary { 50 | background-color: var(--color__background--quaternary); 51 | } 52 | 53 | .app__deco { 54 | position: absolute; 55 | top: 0; 56 | right: 0; 57 | z-index: var(--level-0); 58 | pointer-events: none; 59 | 60 | & path { 61 | opacity: 0.08; 62 | transition: all 2s ease; 63 | fill: #ffffff; 64 | } 65 | } 66 | 67 | .app__deco--default { 68 | & path { 69 | d: path( 70 | 'M294.922 111.746C330.57 175.894 310.047 248.019 247.289 282.894C184.532 317.77 103.141 301.894 67.6569 238.042C39.2199 186.871 54.2894 86.126 109.289 55.5614C164.289 24.9968 266.485 60.5749 294.922 111.746Z' 71 | ); 72 | } 73 | } 74 | 75 | .app__deco--variant1 { 76 | & path { 77 | d: path( 78 | 'M367.393537,0.000001l66.180683,391.154837l-367.393537,62.160436l-66.180683,-391.154837l367.393537,-62.160436Z' 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/css/module.contacts.css: -------------------------------------------------------------------------------- 1 | .contactGroup { 2 | position: relative; 3 | 4 | &::before { 5 | content: ''; 6 | position: absolute; 7 | left: var(--spacing--n20px); 8 | top: 10px; 9 | width: 1px; 10 | height: 100%; 11 | background-color: var(--color__black); 12 | opacity: 0.1; 13 | } 14 | } 15 | 16 | .contactGroup__deco { 17 | position: absolute; 18 | left: -19px; 19 | top: var(--spacing--7px); 20 | transform: translate3d(-50%, 0, 0); 21 | } 22 | 23 | .contactGroup__list { 24 | list-style: none; 25 | padding-left: 0; 26 | margin: 0; 27 | padding-top: var(--spacing--12px); 28 | padding-bottom: var(--spacing--16px); 29 | } 30 | 31 | .contactCard { 32 | border-radius: var(--border__radius--8px); 33 | background-color: rgba(255, 255, 255, 0.3); 34 | padding: var(--spacing--16px) var(--spacing--20px); 35 | display: flex; 36 | justify-content: space-between; 37 | margin-bottom: var(--spacing--8px); 38 | align-items: center; 39 | 40 | &:last-child { 41 | margin-bottom: 0; 42 | } 43 | } 44 | 45 | .contactCard__distance { 46 | display: inline-flex; 47 | align-items: center; 48 | flex: 0; 49 | } 50 | 51 | .contactCard__name { 52 | flex-grow: 1; 53 | padding-right: var(--spacing--12px); 54 | } 55 | 56 | .contactCard__value { 57 | padding-right: var(--spacing--4px); 58 | font-size: 24px; 59 | } 60 | 61 | .contactCard__text { 62 | text-transform: uppercase; 63 | flex: 0; 64 | } 65 | -------------------------------------------------------------------------------- /src/css/module.score.css: -------------------------------------------------------------------------------- 1 | .scoretracker { 2 | width: 55px; 3 | position: relative; 4 | margin-bottom: var(--spacing--8px); 5 | } 6 | 7 | .scoretracker--large { 8 | width: 100px; 9 | margin-bottom: var(--spacing--28px); 10 | left: -6px; 11 | } 12 | 13 | .scoretracker__deco { 14 | position: absolute; 15 | top: 0; 16 | padding: var(--spacing--8px); 17 | } 18 | 19 | .scoretracker__deco--large { 20 | padding: var(--spacing--13px); 21 | } 22 | 23 | .scoretracker__value { 24 | position: absolute; 25 | top: 100%; 26 | left: 0; 27 | right: 0; 28 | text-align: center; 29 | transform: translate3d(0, -50%, 0); 30 | padding-top: var(--spacing--6px); 31 | } 32 | 33 | .scoretracker__value--large { 34 | padding-top: var(--spacing--16px); 35 | } 36 | 37 | .scoretracker__text { 38 | text-transform: uppercase; 39 | display: none; 40 | } 41 | 42 | .scoretracker__text--large { 43 | display: block; 44 | } 45 | -------------------------------------------------------------------------------- /src/css/module.settings.css: -------------------------------------------------------------------------------- 1 | .settingsCard { 2 | background-color: rgba(255, 255, 255, 0.3); 3 | padding: var(--spacing--24px) var(--spacing--40px) 0; 4 | margin-right: var(--spacing--n40px); 5 | margin-left: var(--spacing--n40px); 6 | margin-bottom: var(--spacing--12px); 7 | } 8 | 9 | .settingsCard__control { 10 | color: var(--color__cta); 11 | padding: 0 var(--spacing--40px); 12 | margin-right: var(--spacing--n40px); 13 | margin-left: var(--spacing--n40px); 14 | 15 | & svg { 16 | transition: opacity 0.2s ease; 17 | opacity: 0.2; 18 | } 19 | } 20 | 21 | .settingsCard__label { 22 | flex-grow: 1; 23 | padding-right: var(--spacing--12px); 24 | padding-top: var(--spacing--20px); 25 | padding-bottom: var(--spacing--20px); 26 | cursor: pointer; 27 | display: flex; 28 | 29 | & span { 30 | flex-grow: 1; 31 | } 32 | } 33 | 34 | .settingsCard__number { 35 | letter-spacing: 0.1em; 36 | color: var(--color__text); 37 | text-transform: uppercase; 38 | margin-bottom: var(--spacing--10px); 39 | } 40 | 41 | .settingsCard__control--first { 42 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 43 | } 44 | 45 | .settingsCard__input { 46 | display: none; 47 | 48 | &:checked { 49 | & + label { 50 | & svg { 51 | opacity: 1; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/css/variables.borders.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --border__radius--8px: 8px; 3 | --border__radius--10px: 10px; 4 | --border__radius--12px: 12px; 5 | --border__radius--20px: 20px; 6 | --border__radius--24px: 24px; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/variables.colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color__cta: #ff3c3c; 3 | --color__background--primary: #fac936; 4 | --color__background--secondary: #6a62ff; 5 | --color__background--tertiary: #f84e5e; 6 | --color__background--quaternary: #1c3ba4; 7 | 8 | --color__text: #303548; 9 | 10 | --color__white: #ffffff; 11 | --color__white--transparent: rgba(255, 255, 255, 0.3); 12 | 13 | --color__black: #000000; 14 | 15 | --color__glow--cta: rgba(255, 87, 87, 0.5); 16 | } 17 | -------------------------------------------------------------------------------- /src/css/variables.levels.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --level-0: 0; 3 | --level-1: 1; 4 | --level-2: 2; 5 | --level-3: 3; 6 | --level-4: 4; 7 | --level-5: 5; 8 | --level-6: 6; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/variables.spacing.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --spacing--n20px: -20px; 3 | --spacing--n40px: -40px; 4 | --spacing--2px: 2px; 5 | --spacing--4px: 4px; 6 | --spacing--6px: 6px; 7 | --spacing--7px: 7px; 8 | --spacing--8px: 8px; 9 | --spacing--10px: 10px; 10 | --spacing--12px: 12px; 11 | --spacing--13px: 13px; 12 | --spacing--14px: 14px; 13 | --spacing--16px: 16px; 14 | --spacing--20px: 20px; 15 | --spacing--24px: 24px; 16 | --spacing--28px: 28px; 17 | --spacing--32px: 32px; 18 | --spacing--36px: 36px; 19 | --spacing--40px: 40px; 20 | --spacing--80px: 80px; 21 | --spacing--120px: 120px; 22 | } 23 | -------------------------------------------------------------------------------- /src/css/variables.typography.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font__family--primary: 'Roboto', sans-serif; 3 | --font__family--secondary: 'Roboto Slab', serif; 4 | 5 | --font__weight--light: 300; 6 | --font__weight--regular: 400; 7 | --font__weight--medium: 500; 8 | --font__weight--bold: 700; 9 | 10 | --font__size--xxsmall: 8px; 11 | --font__line--xxsmall: 10px; 12 | 13 | --font__size--xsmall: 12px; 14 | --font__line--xsmall: 16px; 15 | 16 | --font__size--small: 13px; 17 | --font__line--small: 20px; 18 | 19 | --font__size--base: 14px; 20 | --font__line--base: 20px; 21 | 22 | --font__size--large: 18px; 23 | --font__line--large: 20px; 24 | 25 | --font__size--xlarge: 22px; 26 | --font__line--xlarge: 28px; 27 | 28 | --font__size--xxlarge: 32px; 29 | --font__line--xxlarge: 36px; 30 | } 31 | -------------------------------------------------------------------------------- /src/css/vendor.toastify.css: -------------------------------------------------------------------------------- 1 | .Toastify__toast-container { 2 | padding: 0; 3 | position: absolute; 4 | top: 0; 5 | } 6 | 7 | .Toastify__toast { 8 | border-radius: var(--border__radius--20px) var(--border__radius--20px) 0 0; 9 | padding: var(--spacing--16px) var(--spacing--40px); 10 | } 11 | 12 | .Toastify__toast--error { 13 | background-color: var(--color__cta); 14 | color: var(--color__white); 15 | } 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | import * as serviceWorker from './workers/serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | 14 | serviceWorker.register(); 15 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/models/models.ts: -------------------------------------------------------------------------------- 1 | export type CustomHook = () => [S, A]; 2 | -------------------------------------------------------------------------------- /src/modules/activities/components/ActivityList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isEmpty } from 'lodash'; 3 | 4 | import { Card, Button, BUTTON } from 'components'; 5 | import { useActivitiesServices } from 'modules/activities'; 6 | import { ReactComponent as PlusIcon } from 'assets/icons/plus.svg'; 7 | 8 | interface Props { 9 | toggleModalState: VoidFunction; 10 | } 11 | 12 | export const ActivityList: React.FC = ({ toggleModalState }) => { 13 | const [ 14 | { userActivities, isLoading }, 15 | { removeActivity, completeActivity }, 16 | ] = useActivitiesServices(); 17 | 18 | if (isLoading && isEmpty(userActivities)) 19 | return ( 20 | <> 21 | 22 |
23 | 24 |
25 | 26 | 27 | ); 28 | 29 | return ( 30 | <> 31 | {userActivities?.map((activity) => ( 32 | 39 | ))} 40 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/modules/activities/components/ActivityModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Modal } from 'modules/modal'; 4 | import { Button, BUTTON, Heading, HEADING, Tabs } from 'components'; 5 | import { ReactComponent as CloseIcon } from 'assets/icons/close.svg'; 6 | import { AddActivity, ActivitySuggestion } from 'modules/activities'; 7 | 8 | interface Props { 9 | isModalOpen: boolean; 10 | toggleModalState: VoidFunction; 11 | isLight?: boolean; 12 | } 13 | 14 | export const ActivityModal: React.FC = ({ 15 | isModalOpen, 16 | toggleModalState, 17 | isLight, 18 | }) => { 19 | return ( 20 | 21 |
22 |
23 | 33 |
34 | 42 | } 43 | contentSecondary={} 44 | /> 45 |
46 |
47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/modules/activities/components/ActivitySuggestion.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | import { Button, BUTTON } from 'components'; 5 | import { ReactComponent as PlusIconLight } from 'assets/icons/plus.svg'; 6 | import { ReactComponent as PlusIconDark } from 'assets/icons/plus_large.svg'; 7 | import { useSettingsServices } from 'modules/settings'; 8 | import { useActivitiesServices, SUGGESTIONS } from 'modules/activities'; 9 | 10 | interface Props { 11 | isLight?: boolean; 12 | callback: VoidFunction; 13 | } 14 | 15 | export const ActivitySuggestion: React.FC = ({ isLight, callback }) => { 16 | const [, { addActivity }] = useActivitiesServices(); 17 | const [{ userSettings }] = useSettingsServices(); 18 | 19 | const handleClick = React.useCallback( 20 | (e: React.MouseEvent) => { 21 | const { value } = e.currentTarget; 22 | const dateValue = new Date(); 23 | 24 | addActivity({ 25 | date: dateValue, 26 | title: value, 27 | score: Math.floor(Math.random() * 9 + 2), 28 | style: Math.floor(Math.random() * 3), 29 | }); 30 | 31 | callback(); 32 | }, 33 | [addActivity, callback], 34 | ); 35 | 36 | if (isEmpty(userSettings)) return null; 37 | 38 | return ( 39 | <> 40 | {SUGGESTIONS.map(({ value, label, restrictions }, index) => { 41 | if (restrictions) { 42 | if (!userSettings) return null; 43 | for (const key in restrictions) { 44 | if (userSettings[key] !== restrictions[key]) return null; 45 | } 46 | } 47 | 48 | return ( 49 |
50 | 60 |
61 | ); 62 | })} 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/modules/activities/components/ActivitySummary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | import { ReactComponent as PlusIcon } from 'assets/icons/plus_large.svg'; 5 | import { Button, BUTTON, Card, CardSlider, CardSpace } from 'components'; 6 | import { useActivitiesServices, ActivityModal } from 'modules/activities'; 7 | import { useModalControls } from 'modules/modal'; 8 | 9 | export const ActivitySummary = () => { 10 | const [isModalOpen, { toggleModalState }] = useModalControls(); 11 | const [ 12 | { userActivities }, 13 | { removeActivity, completeActivity, getActivities }, 14 | ] = useActivitiesServices(); 15 | 16 | React.useEffect(() => { 17 | getActivities(); 18 | }, [getActivities]); 19 | 20 | if (!userActivities || isEmpty(userActivities)) 21 | return ( 22 |
23 | 27 | 30 | 37 |
38 | ); 39 | 40 | return ( 41 |
42 | 45 | 46 | {userActivities?.map((activity) => ( 47 | 54 | ))} 55 | {userActivities?.length > 2 && } 56 | 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/modules/activities/components/AddActivity.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useForm, FieldValues } from 'react-hook-form'; 3 | 4 | import { useActivitiesServices } from 'modules/activities'; 5 | import { TextInput, BUTTON, Button } from 'components'; 6 | 7 | interface Props { 8 | callback: VoidFunction; 9 | } 10 | 11 | const AddActivity: React.FC = ({ callback }) => { 12 | const [, { addActivity }] = useActivitiesServices(); 13 | const { handleSubmit, register, watch, reset, errors } = useForm(); 14 | 15 | const onSubmit = (values: FieldValues) => { 16 | const { title } = values; 17 | const dateValue = new Date(); 18 | 19 | addActivity({ 20 | date: dateValue, 21 | title, 22 | score: Math.floor(Math.random() * 9 + 2), 23 | style: Math.floor(Math.random() * 3), 24 | }); 25 | 26 | reset(); 27 | callback(); 28 | }; 29 | 30 | const { title } = watch(); 31 | 32 | return ( 33 |
37 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export { AddActivity }; 55 | -------------------------------------------------------------------------------- /src/modules/activities/components/CompletedActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | import { useScoreServices } from 'modules/score'; 5 | import { Card, Button, BUTTON } from 'components'; 6 | 7 | export const CompletedActivity = () => { 8 | const [{ userScore, isLoading }, { getScoreHistory }] = useScoreServices(); 9 | 10 | React.useEffect(() => { 11 | getScoreHistory(); 12 | }, [getScoreHistory]); 13 | 14 | if (isLoading && isEmpty(userScore?.history)) return null; 15 | 16 | if (!userScore || isEmpty(userScore?.history)) 17 | return ( 18 | <> 19 | 20 |
21 | 22 |
23 | 24 | 25 | ); 26 | 27 | return ( 28 |
29 | {userScore.history.map((activity) => ( 30 | 31 | ))} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/modules/activities/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AddActivity'; 2 | export * from './ActivitySummary'; 3 | export * from './ActivityModal'; 4 | export * from './CompletedActivity'; 5 | export * from './ActivityList'; 6 | export * from './ActivitySuggestion'; 7 | -------------------------------------------------------------------------------- /src/modules/activities/constants/constants.ts: -------------------------------------------------------------------------------- 1 | import { Suggestion } from 'modules/activities'; 2 | 3 | export const SUGGESTIONS: Suggestion[] = [ 4 | { 5 | value: 'Excercise', 6 | label: 'Excercise', 7 | }, 8 | { value: 'Eat a healthy meal', label: ' Eat healthy' }, 9 | { value: 'Call a family member or a friend', label: 'Stay in touch' }, 10 | { 11 | value: 'Play a board game with housemates', 12 | label: 'Board game', 13 | restrictions: { hasAssignedSelfIsolation: false, isLivingAlone: false }, 14 | }, 15 | { 16 | value: 'Play an online multiplayer game', 17 | label: 'Online game', 18 | }, 19 | { 20 | value: 'Movie night with housemates', 21 | label: 'Movie night', 22 | restrictions: { hasAssignedSelfIsolation: false, isLivingAlone: false }, 23 | }, 24 | { 25 | value: 'Online movie watching with friends', 26 | label: 'Movie night', 27 | restrictions: { hasAssignedSelfIsolation: true, isLivingAlone: true }, 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/modules/activities/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | -------------------------------------------------------------------------------- /src/modules/activities/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useActivitiesServices'; 2 | -------------------------------------------------------------------------------- /src/modules/activities/hooks/useActivitiesServices.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import * as React from 'react'; 3 | 4 | import { getUserData } from 'modules/user'; 5 | import { 6 | ActivitiesState, 7 | getUserActivities, 8 | getActivitiesState, 9 | addUserActivity, 10 | ActivitiesActionTypes, 11 | removeUserActivity, 12 | completeUserActivity, 13 | } from 'modules/activities'; 14 | import { CustomHook } from 'models'; 15 | 16 | export interface ActivityInput { 17 | date: Date; 18 | title: string; 19 | score: number; 20 | style: number; 21 | } 22 | 23 | interface Api { 24 | getActivities: VoidFunction; 25 | addActivity: (activity: ActivityInput) => void; 26 | removeActivity: (id: string) => void; 27 | completeActivity: (id: string) => void; 28 | } 29 | 30 | export const useActivitiesServices: CustomHook = () => { 31 | const dispatch = useDispatch(); 32 | const { userData } = useSelector(getUserData); 33 | const activities = useSelector(getActivitiesState); 34 | 35 | const getActivities = React.useCallback(async () => { 36 | if (!userData) return; 37 | dispatch({ 38 | type: ActivitiesActionTypes.Request, 39 | }); 40 | 41 | const payload = await getUserActivities(userData); 42 | 43 | dispatch({ 44 | type: ActivitiesActionTypes.Success, 45 | payload: payload, 46 | }); 47 | }, [dispatch, userData]); 48 | 49 | const addActivity = React.useCallback( 50 | async (activity: ActivityInput) => { 51 | if (userData) { 52 | await addUserActivity(userData, activity); 53 | getActivities(); 54 | } 55 | }, 56 | [userData, getActivities], 57 | ); 58 | 59 | const removeActivity = React.useCallback( 60 | async (id: string) => { 61 | if (userData) { 62 | await removeUserActivity(userData, id); 63 | getActivities(); 64 | } 65 | }, 66 | [getActivities, userData], 67 | ); 68 | 69 | const completeActivity = React.useCallback( 70 | async (id: string) => { 71 | if (userData) { 72 | await completeUserActivity(userData, id); 73 | removeActivity(id); 74 | } 75 | }, 76 | [userData, removeActivity], 77 | ); 78 | 79 | const api = React.useMemo( 80 | () => ({ 81 | completeActivity, 82 | getActivities, 83 | addActivity, 84 | removeActivity, 85 | }), 86 | [addActivity, getActivities, removeActivity, completeActivity], 87 | ); 88 | 89 | return [activities, api]; 90 | }; 91 | -------------------------------------------------------------------------------- /src/modules/activities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export * from './components'; 3 | export * from './redux'; 4 | export * from './hooks'; 5 | export * from './models'; 6 | export * from './constants'; 7 | -------------------------------------------------------------------------------- /src/modules/activities/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/activities/models/models.ts: -------------------------------------------------------------------------------- 1 | import { UserSettings } from 'modules/settings'; 2 | 3 | export type UserActivity = { 4 | date: Date; 5 | title: string; 6 | id: string; 7 | score: number; 8 | style: number; 9 | }; 10 | 11 | export interface ActivitiesState { 12 | isLoading: boolean; 13 | userActivities?: UserActivity[]; 14 | error?: string; 15 | } 16 | 17 | export interface Suggestion { 18 | value: string; 19 | label: string; 20 | restrictions?: UserSettings; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/activities/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActivitiesActionTypes, UserActivity } from 'modules/activities'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const ActivitiesActions = { 5 | Request: () => createAction(ActivitiesActionTypes.Request), 6 | 7 | Success: (settings: UserActivity[]) => 8 | createAction(ActivitiesActionTypes.Success, settings), 9 | 10 | Error: (error?: string) => createAction(ActivitiesActionTypes.Error, error), 11 | Reset: () => createAction(ActivitiesActionTypes.Reset), 12 | }; 13 | 14 | export type ActivitiesActions = ActionUnion; 15 | -------------------------------------------------------------------------------- /src/modules/activities/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/activities/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivitiesActionTypes, 3 | ActivitiesActions, 4 | ActivitiesState, 5 | } from 'modules/activities'; 6 | 7 | const INITIAL_STATE: ActivitiesState = { 8 | userActivities: undefined, 9 | isLoading: false, 10 | error: undefined, 11 | }; 12 | 13 | export const activitiesReducer = ( 14 | state: ActivitiesState = INITIAL_STATE, 15 | action: ActivitiesActions, 16 | ): ActivitiesState => { 17 | switch (action.type) { 18 | case ActivitiesActionTypes.Request: 19 | return { 20 | ...state, 21 | isLoading: true, 22 | error: undefined, 23 | }; 24 | case ActivitiesActionTypes.Success: 25 | return { 26 | ...state, 27 | userActivities: action.payload, 28 | isLoading: false, 29 | }; 30 | case ActivitiesActionTypes.Error: 31 | return { 32 | ...state, 33 | error: action.payload, 34 | isLoading: false, 35 | }; 36 | case ActivitiesActionTypes.Reset: 37 | return INITIAL_STATE; 38 | default: 39 | return state || INITIAL_STATE; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/modules/activities/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getActivitiesState = ({ activities }: ApplicationState) => 4 | activities; 5 | -------------------------------------------------------------------------------- /src/modules/activities/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetActivitiesActionTypes { 2 | Request = 'ACTIVITIES_DATA_REQUEST', 3 | Success = 'ACTIVITIES_DATA_SUCCESS', 4 | Error = 'ACTIVITIES_DATA_ERROR', 5 | Reset = 'ACTIVITIES_DATA_RESET', 6 | } 7 | 8 | export const ActivitiesActionTypes = { 9 | ...GetActivitiesActionTypes, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/activities/services/addActivity.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreService } from 'modules/firebase'; 2 | import { updateScoreHistory } from 'modules/score'; 3 | import { ActivityInput } from 'modules/activities'; 4 | 5 | const addUserActivity = (user: firebase.UserInfo, value: ActivityInput) => { 6 | const firestore = new FirestoreService(`activities/${user.uid}/userActivity`); 7 | 8 | firestore.addAsync(value); 9 | }; 10 | 11 | const removeUserActivity = (user: firebase.UserInfo, id: string) => { 12 | const firestore = new FirestoreService(`activities/${user.uid}/userActivity`); 13 | 14 | firestore.removeAsync(id); 15 | }; 16 | 17 | const completeUserActivity = async (user: firebase.UserInfo, id: string) => { 18 | const firestore = new FirestoreService(`activities/${user.uid}/userActivity`); 19 | 20 | const completedTask = await firestore.getByIdAsync(id); 21 | 22 | delete completedTask.id; 23 | 24 | await updateScoreHistory(user, completedTask); 25 | }; 26 | 27 | export { addUserActivity, completeUserActivity, removeUserActivity }; 28 | -------------------------------------------------------------------------------- /src/modules/activities/services/getActivities.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreService } from 'modules/firebase'; 2 | 3 | const getUserActivities = (user: firebase.UserInfo) => { 4 | const firestore = new FirestoreService('activities'); 5 | 6 | return firestore.getSubcollection(user.uid, 'userActivity'); 7 | }; 8 | 9 | export { getUserActivities }; 10 | -------------------------------------------------------------------------------- /src/modules/activities/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addActivity'; 2 | export * from './getActivities'; 3 | -------------------------------------------------------------------------------- /src/modules/app/components/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToastContainer, Slide } from 'react-toastify'; 3 | import 'react-toastify/dist/ReactToastify.min.css'; 4 | 5 | import { Routing } from 'modules/routing'; 6 | import { useAppState } from 'modules/app'; 7 | import { Navigation } from 'components/Navigation'; 8 | 9 | export const AppLayout: React.FC = () => { 10 | const [{ theme }] = useAppState(); 11 | 12 | const appClassName = theme.showNav 13 | ? 'app__background app__background--primary app__background--withNav' 14 | : 'app__background app__background--primary'; 15 | return ( 16 | <> 17 |
22 | 23 | 24 | 25 |
26 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/modules/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppLayout'; 2 | -------------------------------------------------------------------------------- /src/modules/app/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAppState'; 2 | -------------------------------------------------------------------------------- /src/modules/app/hooks/useAppState.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { CustomHook } from 'models'; 5 | import { Theme, getAppConfig, AppActionTypes, AppState } from 'modules/app'; 6 | 7 | interface Api { 8 | setAppTheme: (theme: Theme) => void; 9 | } 10 | 11 | export const useAppState: CustomHook = () => { 12 | const dispatch = useDispatch(); 13 | const state = useSelector(getAppConfig); 14 | 15 | const setAppTheme = React.useCallback( 16 | (theme: Theme) => { 17 | dispatch({ type: AppActionTypes.Request }); 18 | dispatch({ type: AppActionTypes.Success, payload: { theme } }); 19 | }, 20 | [dispatch], 21 | ); 22 | 23 | const api = React.useMemo( 24 | () => ({ 25 | setAppTheme, 26 | }), 27 | [setAppTheme], 28 | ); 29 | 30 | return [state, api]; 31 | }; 32 | -------------------------------------------------------------------------------- /src/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './redux'; 3 | export * from './hooks'; 4 | export * from './models'; 5 | -------------------------------------------------------------------------------- /src/modules/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/app/models/models.ts: -------------------------------------------------------------------------------- 1 | export interface Theme { 2 | color: string; 3 | showNav: boolean; 4 | shapeClass: string; 5 | } 6 | 7 | export interface AppVariables { 8 | theme: Theme; 9 | } 10 | 11 | export type AppState = AppVariables & { 12 | isLoading: boolean; 13 | error?: string; 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/app/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { AppActionTypes, AppVariables } from 'modules/app'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const AppActions = { 5 | Request: () => createAction(AppActionTypes.Request), 6 | 7 | Success: (settings: AppVariables) => 8 | createAction(AppActionTypes.Success, settings), 9 | 10 | Error: (error?: string) => createAction(AppActionTypes.Error, error), 11 | }; 12 | 13 | export type AppActions = ActionUnion; 14 | -------------------------------------------------------------------------------- /src/modules/app/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/app/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AppActionTypes, AppActions, AppState } from 'modules/app'; 2 | 3 | const INITIAL_STATE: AppState = { 4 | theme: { 5 | showNav: false, 6 | color: '#FAC936', 7 | shapeClass: '', 8 | }, 9 | isLoading: false, 10 | error: undefined, 11 | }; 12 | 13 | export const appReducer = ( 14 | state: AppState = INITIAL_STATE, 15 | action: AppActions, 16 | ): AppState => { 17 | switch (action.type) { 18 | case AppActionTypes.Request: 19 | return { 20 | ...state, 21 | isLoading: true, 22 | error: undefined, 23 | }; 24 | case AppActionTypes.Success: 25 | return { 26 | ...state, 27 | ...action.payload, 28 | isLoading: false, 29 | }; 30 | case AppActionTypes.Error: 31 | return { 32 | ...state, 33 | error: action.payload, 34 | isLoading: false, 35 | }; 36 | default: 37 | return state || INITIAL_STATE; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/modules/app/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getAppConfig = ({ app }: ApplicationState) => app; 4 | -------------------------------------------------------------------------------- /src/modules/app/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetAppActionTypes { 2 | Request = 'APP_UPDATE_REQUEST', 3 | Success = 'APP_UPDATE_SUCCESS', 4 | Error = 'APP_DATA_ERROR', 5 | } 6 | 7 | export const AppActionTypes = { 8 | ...GetAppActionTypes, 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/assistant/components/ChatControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button, Letter, BUTTON } from 'components'; 4 | 5 | interface Props { 6 | answerFirst: string; 7 | answerSecond: string; 8 | onClick: (message: string) => void; 9 | } 10 | 11 | type AnswerState = 'Yes' | 'No' | 'Waiting For Answer'; 12 | 13 | export const ChatControls: React.FC = ({ 14 | answerFirst, 15 | answerSecond, 16 | onClick, 17 | }) => { 18 | const [selectedAnswer, setSelectedAnswer] = React.useState( 19 | 'Waiting For Answer', 20 | ); 21 | 22 | const handleAnswer = React.useCallback( 23 | (e: React.MouseEvent) => { 24 | if (selectedAnswer !== 'Waiting For Answer') return; 25 | const { value, dataset } = e.currentTarget; 26 | const { message } = dataset; 27 | setSelectedAnswer(value as AnswerState); 28 | onClick(message || ''); 29 | }, 30 | [onClick, selectedAnswer], 31 | ); 32 | 33 | return ( 34 |
35 |
36 | 51 |
52 | 53 |
54 | 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/modules/assistant/components/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | origin: 'user' | 'bot'; 5 | } 6 | 7 | export const ChatMessage: React.FC = ({ children, origin }) => { 8 | const className = 9 | origin === 'user' 10 | ? 'chat__message chat__message--animated chat__message--user' 11 | : 'chat__message chat__message--animated chat__message--bot'; 12 | 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/assistant/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ChatMessage'; 2 | export * from './ChatControls'; 3 | -------------------------------------------------------------------------------- /src/modules/assistant/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAssistant'; 2 | -------------------------------------------------------------------------------- /src/modules/assistant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './hooks'; 3 | export * from './redux'; 4 | -------------------------------------------------------------------------------- /src/modules/assistant/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'twilio-chat/lib/message'; 2 | 3 | import { ActionUnion, createAction } from 'modules/redux-store'; 4 | 5 | import { AssistantActionTypes } from './types'; 6 | 7 | export const AssistantActions = { 8 | Request: () => createAction(AssistantActionTypes.Request), 9 | 10 | Success: (message: Message) => 11 | createAction(AssistantActionTypes.Success, { message }), 12 | 13 | UpdateToken: (token: string) => 14 | createAction(AssistantActionTypes.UpdateToken, { token }), 15 | 16 | ClearReducer: () => createAction(AssistantActionTypes.ClearReducer), 17 | 18 | Error: (error?: string) => 19 | createAction(AssistantActionTypes.Error, { error }), 20 | }; 21 | 22 | export type AssistantActions = ActionUnion; 23 | -------------------------------------------------------------------------------- /src/modules/assistant/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/assistant/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'twilio-chat/lib/message'; 2 | 3 | import { AssistantActions } from './actions'; 4 | import { AssistantActionTypes } from './types'; 5 | 6 | export interface AssistantState { 7 | messages: Message[]; 8 | token: string; 9 | isLoading: boolean; 10 | error?: string; 11 | } 12 | 13 | const INITIAL_STATE: AssistantState = { 14 | messages: [], 15 | token: '', 16 | isLoading: false, 17 | error: undefined, 18 | }; 19 | 20 | export const assistantReducer = ( 21 | state: AssistantState = INITIAL_STATE, 22 | action: AssistantActions, 23 | ): AssistantState => { 24 | switch (action.type) { 25 | case AssistantActionTypes.Request: 26 | return { 27 | ...state, 28 | isLoading: true, 29 | error: undefined, 30 | }; 31 | case AssistantActionTypes.Success: 32 | return { 33 | ...state, 34 | messages: [...state.messages, action.payload.message], 35 | isLoading: false, 36 | }; 37 | case AssistantActionTypes.UpdateToken: 38 | return { 39 | ...state, 40 | token: action.payload.token, 41 | }; 42 | 43 | case AssistantActionTypes.Error: 44 | return { 45 | ...state, 46 | error: action.payload.error, 47 | isLoading: false, 48 | }; 49 | case AssistantActionTypes.ClearReducer: 50 | return INITIAL_STATE; 51 | default: 52 | return state || INITIAL_STATE; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/modules/assistant/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getAssistantData = ({ assistant }: ApplicationState) => assistant; 4 | -------------------------------------------------------------------------------- /src/modules/assistant/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetAssistantActionTypes { 2 | Request = 'ASSISTANT_REQUEST', 3 | Success = 'ASSISTANT_SUCCESS', 4 | Error = 'ASSISTANT_ERROR', 5 | UpdateToken = 'ASSISTANT_UPDATE_TOKEN', 6 | ClearReducer = 'ASSISTANT_CLEAR_REDUCER', 7 | } 8 | 9 | export const AssistantActionTypes = { 10 | ...GetAssistantActionTypes, 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/contacts/components/AddContact.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { format, add, isToday } from 'date-fns'; 3 | import { useForm, FieldValues } from 'react-hook-form'; 4 | 5 | import { useContactsServices } from 'modules/contacts'; 6 | import { TextInput, BUTTON, Button } from 'components'; 7 | 8 | interface Props { 9 | callback: VoidFunction; 10 | } 11 | 12 | const AddContact: React.FC = ({ callback }) => { 13 | const { handleSubmit, register, watch, reset, errors } = useForm(); 14 | const [, { addContact }] = useContactsServices(); 15 | 16 | const onSubmit = (values: FieldValues) => { 17 | const { date, name, phoneNumber } = values; 18 | 19 | const dateValue = new Date(); 20 | 21 | const newDate = isToday(new Date(date)) 22 | ? add(new Date(date).setHours(0, 0, 0, 0), { 23 | hours: dateValue.getHours(), 24 | minutes: dateValue.getMinutes(), 25 | seconds: dateValue.getSeconds(), 26 | }) 27 | : new Date(date); 28 | addContact({ 29 | date: newDate, 30 | name, 31 | phoneNumber, 32 | }); 33 | reset(); 34 | callback(); 35 | }; 36 | 37 | const pattern = /^\+((?:9[679]|8[035789]|6[789]|5[90]|42|3[578]|2[1-689])|9[0-58]|8[1246]|6[0-6]|5[1-8]|4[013-9]|3[0-469]|2[70]|7|1)(?:\W*\d){0,13}\d$/gm; 38 | 39 | const { date, name, phoneNumber } = watch(); 40 | 41 | return ( 42 |
43 |
44 | 54 | 66 | 79 |
80 | 81 | 82 |
83 | ); 84 | }; 85 | 86 | export { AddContact }; 87 | -------------------------------------------------------------------------------- /src/modules/contacts/components/AddContactModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Modal } from 'modules/modal'; 4 | import { Button, BUTTON, Heading, HEADING } from 'components'; 5 | import { AddContact } from 'modules/contacts'; 6 | import { ReactComponent as CloseIcon } from 'assets/icons/close.svg'; 7 | 8 | interface Props { 9 | isModalOpen: boolean; 10 | toggleModalState: VoidFunction; 11 | } 12 | 13 | export const AddContactModal: React.FC = ({ 14 | isModalOpen, 15 | toggleModalState, 16 | }) => { 17 | return ( 18 | 19 |
20 | 30 |
31 | Keep track of people you see and don't forget to check up on them 32 |
33 |
34 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/modules/contacts/components/ContactCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format } from 'date-fns'; 3 | 4 | import { Button, BUTTON } from 'components'; 5 | import { ReactComponent as TextBubble } from 'assets/icons/text_bubble.svg'; 6 | import { ReactComponent as CircleIcon } from 'assets/icons/circle_ring.svg'; 7 | import { ReactComponent as TrashIcon } from 'assets/icons/trash.svg'; 8 | import { UserContact } from 'modules/contacts'; 9 | 10 | interface Props { 11 | date: Date; 12 | contacts: UserContact[]; 13 | removeContact: (id: string) => void; 14 | setCurrentContact: (contact: UserContact) => void; 15 | } 16 | 17 | export const ContactCard: React.FC = ({ 18 | date, 19 | contacts, 20 | removeContact, 21 | setCurrentContact, 22 | }) => { 23 | const getContactCard = (contact: UserContact) => ( 24 |
  • 25 |
    26 |
    33 | {contact.phoneNumber && ( 34 |
  • 41 | ); 42 | return ( 43 |
    44 | 45 | 46 | {format(date, 'dd')} 47 | {' '} 48 | {format(date, 'MMM')} 49 |
      {contacts.map(getContactCard)}
    50 |
    51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/modules/contacts/components/ContactGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { UserContact, ContactCard } from 'modules/contacts'; 4 | import { GroupedContacts } from 'views'; 5 | 6 | interface Props { 7 | groupedContacts: GroupedContacts; 8 | removeContact: (id: string) => void; 9 | setCurrentContact: (contact: UserContact) => void; 10 | } 11 | 12 | export const ContactGroup: React.FC = ({ 13 | groupedContacts, 14 | removeContact, 15 | setCurrentContact, 16 | }) => { 17 | const getContactCard = React.useCallback( 18 | (key: string) => ( 19 | 25 | ), 26 | [groupedContacts, removeContact, setCurrentContact], 27 | ); 28 | 29 | const sortedContactCards = React.useMemo( 30 | () => Object.keys(groupedContacts).map(getContactCard), 31 | [getContactCard, groupedContacts], 32 | ); 33 | 34 | return <>{sortedContactCards}; 35 | }; 36 | -------------------------------------------------------------------------------- /src/modules/contacts/components/ContactModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button, BUTTON, Heading, HEADING } from 'components'; 4 | import { Modal } from 'modules/modal'; 5 | import { MessageForm, UserContact } from 'modules/contacts'; 6 | import { ReactComponent as CloseIcon } from 'assets/icons/close.svg'; 7 | 8 | interface Props { 9 | currentContact?: UserContact; 10 | setCurrentContact: (value?: UserContact) => void; 11 | } 12 | 13 | export const ContactModal: React.FC = ({ 14 | currentContact, 15 | setCurrentContact, 16 | }) => { 17 | return ( 18 | 19 |
    20 | 30 |
    31 | Keep track of people you see and don't forget to check up on them. 32 |
    33 |
    34 | {currentContact && } 35 |
    36 |
    37 |
    38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/modules/contacts/components/ContactSummary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { differenceInCalendarDays } from 'date-fns'; 3 | 4 | import { useContactsServices, AddContactModal } from 'modules/contacts'; 5 | import { toDate } from 'util/time'; 6 | import { Button, BUTTON } from 'components'; 7 | import { ReactComponent as PlusIcon } from 'assets/icons/plus_large.svg'; 8 | import { useModalControls } from 'modules/modal'; 9 | 10 | export const ContactSummary = () => { 11 | const [isModalOpen, { toggleModalState }] = useModalControls(); 12 | const [{ userContacts }, { getLastUserContacts }] = useContactsServices(); 13 | 14 | React.useEffect(() => { 15 | getLastUserContacts(); 16 | }, [getLastUserContacts]); 17 | 18 | if (!userContacts || userContacts.length === 0) 19 | return ( 20 |
    21 | 24 | 31 | 35 |
    36 | ); 37 | 38 | if (userContacts.length > 2) userContacts.length = 2; 39 | 40 | return ( 41 |
    42 | 45 | {userContacts.map(({ name, date, id }) => { 46 | const distance = differenceInCalendarDays(new Date(), toDate(date)); 47 | 48 | const distanceValue = ( 49 |
    50 | 51 | {distance} 52 | 53 | 54 | 55 | {distance === 1 ? 'day ago' : 'days ago'} 56 | 57 |
    58 | ); 59 | 60 | return ( 61 |
    62 |
    {name}
    63 | {distanceValue} 64 |
    65 | ); 66 | })} 67 |
    68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/modules/contacts/components/GroupedContacts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { differenceInCalendarDays } from 'date-fns'; 3 | 4 | import { useContactsServices } from 'modules/contacts'; 5 | import { toDate } from 'util/time'; 6 | 7 | export const GroupedContacts = () => { 8 | const [{ userContacts }] = useContactsServices(); 9 | if (!userContacts || userContacts.length === 0) return null; 10 | 11 | return ( 12 | <> 13 | {userContacts.map(({ name, date, id }) => { 14 | const distance = differenceInCalendarDays(new Date(), toDate(date)); 15 | 16 | const distanceValue = ( 17 |
    18 | 19 | {distance} 20 | 21 | 22 | 23 | {distance === 1 ? 'day ago' : 'days ago'} 24 | 25 |
    26 | ); 27 | 28 | return ( 29 |
    30 |
    {name}
    31 | {distanceValue} 32 |
    33 | ); 34 | })} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/modules/contacts/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AddContact'; 2 | export * from './MessageForm'; 3 | export * from './ContactSummary'; 4 | export * from './AddContactModal'; 5 | export * from './ContactModal'; 6 | export * from './ContactGroup'; 7 | export * from './ContactCard'; 8 | -------------------------------------------------------------------------------- /src/modules/contacts/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useContactsServices'; 2 | export * from './useMessageService'; 3 | -------------------------------------------------------------------------------- /src/modules/contacts/hooks/useContactsServices.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import * as React from 'react'; 3 | 4 | import { CustomHook } from 'models'; 5 | import { getUserData } from 'modules/user'; 6 | import { 7 | ContactsState, 8 | getContactsState, 9 | addUserContact, 10 | ContactsActionTypes, 11 | removeUserContact, 12 | getLastContacts, 13 | UserContact, 14 | } from 'modules/contacts'; 15 | 16 | export interface ContactInput { 17 | date: Date; 18 | name: string; 19 | phoneNumber?: string; 20 | } 21 | 22 | interface Api { 23 | getLastUserContacts: (n?: number) => void; 24 | addContact: (contact: ContactInput) => void; 25 | removeContact: (id: string) => void; 26 | } 27 | 28 | export const useContactsServices: CustomHook = () => { 29 | const dispatch = useDispatch(); 30 | const { userData } = useSelector(getUserData); 31 | const contacts = useSelector(getContactsState); 32 | 33 | const successFunction = React.useCallback( 34 | (payload: UserContact[]) => { 35 | dispatch({ 36 | type: ContactsActionTypes.Success, 37 | payload, 38 | }); 39 | }, 40 | [dispatch], 41 | ); 42 | 43 | const errorFunction = React.useCallback( 44 | (payload: string) => { 45 | dispatch({ 46 | type: ContactsActionTypes.Error, 47 | payload, 48 | }); 49 | }, 50 | [dispatch], 51 | ); 52 | 53 | const getLastUserContacts = React.useCallback(async () => { 54 | if (!userData) return; 55 | dispatch({ 56 | type: ContactsActionTypes.Request, 57 | }); 58 | await getLastContacts(userData, { 59 | successFunction, 60 | errorFunction, 61 | }); 62 | }, [dispatch, errorFunction, successFunction, userData]); 63 | 64 | const addContact = React.useCallback( 65 | async (contact: ContactInput) => { 66 | if (userData) { 67 | await addUserContact(userData, contact); 68 | getLastUserContacts(); 69 | } 70 | }, 71 | [userData, getLastUserContacts], 72 | ); 73 | 74 | const removeContact = React.useCallback( 75 | async (id: string) => { 76 | if (userData) { 77 | await removeUserContact(userData, id); 78 | getLastUserContacts(); 79 | } 80 | }, 81 | [getLastUserContacts, userData], 82 | ); 83 | 84 | const api = React.useMemo( 85 | () => ({ 86 | getLastUserContacts, 87 | addContact, 88 | removeContact, 89 | }), 90 | [addContact, removeContact, getLastUserContacts], 91 | ); 92 | 93 | return [contacts, api]; 94 | }; 95 | -------------------------------------------------------------------------------- /src/modules/contacts/hooks/useMessageService.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toast, ToastType, ToastPosition } from 'react-toastify'; 3 | 4 | import { CustomHook } from 'models'; 5 | import { messageContact } from 'modules/contacts'; 6 | 7 | interface State { 8 | hasError: boolean; 9 | isSubmitting: boolean; 10 | submitSuccess: boolean; 11 | } 12 | 13 | export interface Message { 14 | to: string; 15 | body: string; 16 | } 17 | 18 | interface Api { 19 | sendMessage: (message: Message) => void; 20 | } 21 | 22 | type FormatState = (error: boolean, submit: boolean, success: boolean) => State; 23 | 24 | export const useMessageService: CustomHook = () => { 25 | const formatState = React.useCallback( 26 | (hasError, isSubmitting, submitSuccess) => ({ 27 | hasError, 28 | isSubmitting, 29 | submitSuccess, 30 | }), 31 | [], 32 | ); 33 | const [hookState, setHookState] = React.useState( 34 | formatState(false, false, false), 35 | ); 36 | 37 | const handleSuccess = React.useCallback(() => { 38 | setHookState(formatState(false, false, true)); 39 | toast('SMS Message has been sent successfully', { 40 | closeButton: false, 41 | position: ToastPosition.TOP_CENTER, 42 | type: ToastType.SUCCESS, 43 | }); 44 | }, [formatState]); 45 | 46 | const handleError = React.useCallback(() => { 47 | toast('Uh oh... SMS message could not be sent. Try again later.', { 48 | closeButton: false, 49 | position: ToastPosition.TOP_CENTER, 50 | type: ToastType.ERROR, 51 | }); 52 | setHookState(formatState(true, false, false)); 53 | }, [formatState]); 54 | 55 | const sendMessage = React.useCallback( 56 | (message: Message) => { 57 | setHookState(formatState(false, true, false)); 58 | messageContact(message, handleSuccess, handleError); 59 | }, 60 | [formatState, handleError, handleSuccess], 61 | ); 62 | 63 | const api = React.useMemo( 64 | () => ({ 65 | sendMessage, 66 | }), 67 | [sendMessage], 68 | ); 69 | 70 | return [hookState, api]; 71 | }; 72 | -------------------------------------------------------------------------------- /src/modules/contacts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export * from './components'; 3 | export * from './redux'; 4 | export * from './hooks'; 5 | export * from './models'; 6 | -------------------------------------------------------------------------------- /src/modules/contacts/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/contacts/models/models.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from 'util/time'; 2 | 3 | export type UserContact = { 4 | date: Timestamp; 5 | name: string; 6 | id: string; 7 | phoneNumber?: string; 8 | }; 9 | 10 | export interface ContactsState { 11 | isLoading: boolean; 12 | userContacts?: UserContact[]; 13 | error?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/contacts/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { ContactsActionTypes, UserContact } from 'modules/contacts'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const ContactsActions = { 5 | Request: () => createAction(ContactsActionTypes.Request), 6 | 7 | Success: (contacts: UserContact[]) => 8 | createAction(ContactsActionTypes.Success, contacts), 9 | 10 | Error: (error?: string) => createAction(ContactsActionTypes.Error, error), 11 | Reset: () => createAction(ContactsActionTypes.Reset), 12 | }; 13 | 14 | export type ContactsActions = ActionUnion; 15 | -------------------------------------------------------------------------------- /src/modules/contacts/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/contacts/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContactsActionTypes, 3 | ContactsActions, 4 | ContactsState, 5 | } from 'modules/contacts'; 6 | 7 | const INITIAL_STATE: ContactsState = { 8 | userContacts: undefined, 9 | isLoading: false, 10 | error: undefined, 11 | }; 12 | 13 | export const contactsReducer = ( 14 | state: ContactsState = INITIAL_STATE, 15 | action: ContactsActions, 16 | ): ContactsState => { 17 | switch (action.type) { 18 | case ContactsActionTypes.Request: 19 | return { 20 | ...state, 21 | isLoading: true, 22 | error: undefined, 23 | }; 24 | case ContactsActionTypes.Success: 25 | return { 26 | ...state, 27 | userContacts: action.payload, 28 | isLoading: false, 29 | }; 30 | case ContactsActionTypes.Error: 31 | return { 32 | ...state, 33 | error: action.payload, 34 | isLoading: false, 35 | }; 36 | case ContactsActionTypes.Reset: 37 | return INITIAL_STATE; 38 | default: 39 | return state || INITIAL_STATE; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/modules/contacts/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getContactsState = ({ contacts }: ApplicationState) => contacts; 4 | -------------------------------------------------------------------------------- /src/modules/contacts/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetContactsActionTypes { 2 | Request = 'CONTACTS_DATA_REQUEST', 3 | Success = 'CONTACTS_DATA_SUCCESS', 4 | Error = 'CONTACTS_DATA_ERROR', 5 | Reset = 'CONTACTS_DATA_RESET', 6 | } 7 | 8 | export const ContactsActionTypes = { 9 | ...GetContactsActionTypes, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/contacts/services/addContact.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreService } from 'modules/firebase'; 2 | import { ContactInput } from 'modules/contacts'; 3 | 4 | const addUserContact = (user: firebase.UserInfo, value: ContactInput) => { 5 | const firestore = new FirestoreService(`contacts/${user.uid}/userContacts`); 6 | 7 | firestore.addAsync(value); 8 | }; 9 | 10 | const removeUserContact = (user: firebase.UserInfo, id: string) => { 11 | const firestore = new FirestoreService(`contacts/${user.uid}/userContacts`); 12 | 13 | firestore.removeAsync(id); 14 | }; 15 | 16 | export { addUserContact, removeUserContact }; 17 | -------------------------------------------------------------------------------- /src/modules/contacts/services/getContacts.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreService, ListenerProps } from 'modules/firebase'; 2 | 3 | const getLastContacts = ( 4 | user: firebase.UserInfo, 5 | listenerProps: ListenerProps, 6 | ) => { 7 | const firestore = new FirestoreService(`contacts/${user.uid}/userContacts`); 8 | 9 | return firestore.filterByDate(listenerProps); 10 | }; 11 | 12 | export { getLastContacts }; 13 | -------------------------------------------------------------------------------- /src/modules/contacts/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addContact'; 2 | export * from './getContacts'; 3 | export * from './messageContact'; 4 | -------------------------------------------------------------------------------- /src/modules/contacts/services/messageContact.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'modules/contacts'; 2 | import { FirebaseService } from 'modules/firebase'; 3 | 4 | type MessagingFuncion = ( 5 | message: Message, 6 | handleSuccess: VoidFunction, 7 | handleError: VoidFunction, 8 | ) => void; 9 | 10 | const messageContact: MessagingFuncion = async ( 11 | message, 12 | handleSuccess, 13 | handleError, 14 | ) => { 15 | const functions = FirebaseService.FunctionsProvider; 16 | 17 | const { 18 | NODE_ENV, 19 | REACT_APP_TWILIO_PHONE_NUMBER, 20 | REACT_APP_TEST_PHONE_NUMBER, 21 | } = process.env; 22 | 23 | const sendSMSMessage = functions.httpsCallable('sendSMSMessage'); 24 | 25 | const isDev = NODE_ENV === 'development'; 26 | const res = await sendSMSMessage({ 27 | from: REACT_APP_TWILIO_PHONE_NUMBER, 28 | to: isDev ? REACT_APP_TEST_PHONE_NUMBER : message.to, 29 | body: message.body, 30 | }); 31 | 32 | const { success } = res.data; 33 | success ? handleSuccess() : handleError(); 34 | }; 35 | 36 | export { messageContact }; 37 | -------------------------------------------------------------------------------- /src/modules/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export * from './models'; 3 | -------------------------------------------------------------------------------- /src/modules/firebase/models/Collections.ts: -------------------------------------------------------------------------------- 1 | export type Collections = 2 | | 'users' 3 | | 'settings' 4 | | 'score' 5 | | `activities` 6 | | string; 7 | -------------------------------------------------------------------------------- /src/modules/firebase/models/ListenerProps.ts: -------------------------------------------------------------------------------- 1 | export interface ListenerProps { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | successFunction: (data: any) => void; 4 | errorFunction: (error: string) => void; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/firebase/models/Ordering.ts: -------------------------------------------------------------------------------- 1 | export type Order = 'asc' | 'desc'; 2 | 3 | export interface Ordering { 4 | orderBy: string; 5 | order: Order; 6 | limit: number; 7 | startAfter: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/firebase/models/QueryFilter.ts: -------------------------------------------------------------------------------- 1 | export interface QueryFilter { 2 | field: string; 3 | operator: '==' | '>=' | '<=' | '<' | '>' | 'array-contains'; 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | value: any; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/firebase/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './QueryFilter'; 2 | export * from './ListenerProps'; 3 | export * from './Collections'; 4 | export * from './Ordering'; 5 | -------------------------------------------------------------------------------- /src/modules/firebase/services/firebaseService.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | 4 | export class FirebaseService { 5 | private static instance: firebase.app.App; 6 | private static firebaseConfig = { 7 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 8 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 9 | databaseURL: process.env.REACT_APP_DATABASE_URL, 10 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 11 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 12 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 13 | appId: process.env.REACT_APP_FIREBASE_APP_ID, 14 | }; 15 | 16 | public static get AuthProvider() { 17 | return this.Instance.auth(); 18 | } 19 | 20 | public static get FunctionsProvider() { 21 | const functions = this.Instance.functions(); 22 | if (process.env.NODE_ENV === 'development') { 23 | functions.useFunctionsEmulator('http://localhost:5001'); 24 | } 25 | 26 | return functions; 27 | } 28 | 29 | public static get AuthProviderGoogle() { 30 | return new firebase.auth.GoogleAuthProvider(); 31 | } 32 | 33 | public static get AuthProviderEmail() { 34 | return firebase.auth.EmailAuthProvider; 35 | } 36 | 37 | public static get Instance() { 38 | return ( 39 | this.instance || 40 | (this.instance = firebase.initializeApp(this.firebaseConfig)) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/firebase/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './firebaseService'; 2 | export * from './firestoreService'; 3 | -------------------------------------------------------------------------------- /src/modules/modal/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useAppState } from 'modules/app'; 4 | 5 | import Portal from './Portal'; 6 | 7 | interface Props { 8 | isModalOpen: boolean; 9 | } 10 | 11 | export const Modal: React.FC = ({ children, isModalOpen }) => { 12 | const [{ theme }] = useAppState(); 13 | const modalClassName = isModalOpen ? 'modal modal--open' : 'modal'; 14 | return ( 15 | 16 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/modal/components/Portal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | import { usePortal } from 'modules/modal'; 5 | 6 | interface Props { 7 | id: string; 8 | } 9 | 10 | const Portal: React.FC = ({ id, children }) => { 11 | const target = usePortal(id); 12 | return createPortal(children, target); 13 | }; 14 | 15 | export default Portal; 16 | -------------------------------------------------------------------------------- /src/modules/modal/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Modal'; 2 | -------------------------------------------------------------------------------- /src/modules/modal/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useModal'; 2 | export * from './useModalControls'; 3 | -------------------------------------------------------------------------------- /src/modules/modal/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | /** 4 | * Creates DOM element to be used as React root. 5 | * @returns {HTMLElement} 6 | */ 7 | function createRootElement(id: string) { 8 | const rootContainer = document.createElement('div'); 9 | rootContainer.setAttribute('id', id); 10 | return rootContainer; 11 | } 12 | 13 | /** 14 | * Appends element as last child of body. 15 | * @param {HTMLElement} rootElem 16 | */ 17 | function addRootElement(rootElem: any) { 18 | if (document.body && document.body.lastElementChild) 19 | document.body.insertBefore( 20 | rootElem, 21 | document.body.lastElementChild.nextElementSibling, 22 | ); 23 | } 24 | 25 | /** 26 | * Hook to create a React Portal. 27 | * Automatically handles creating and tearing-down the root elements (no SRR 28 | * makes this trivial), so there is no need to ensure the parent target already 29 | * exists. 30 | * @example 31 | * const target = usePortal(id, [id]); 32 | * return createPortal(children, target); 33 | * @param {String} id The id of the target container, e.g 'modal' or 'spotlight' 34 | * @returns {HTMLElement} The DOM node to use as the Portal target. 35 | */ 36 | export function usePortal(id: string) { 37 | const rootElemRef = useRef(null); 38 | 39 | useEffect( 40 | function setupElement() { 41 | // Look for existing target dom element to append to 42 | const existingParent = document.querySelector(`#${id}`); 43 | // Parent is either a new root or the existing dom element 44 | const parentElem = existingParent || createRootElement(id); 45 | 46 | // If there is no existing DOM element, add a new one. 47 | if (!existingParent) { 48 | addRootElement(parentElem); 49 | } 50 | 51 | // Add the detached element to the parent 52 | if (rootElemRef.current) parentElem.appendChild(rootElemRef.current); 53 | 54 | return function removeElement() { 55 | if (rootElemRef.current) rootElemRef.current.remove(); 56 | if (parentElem.childNodes.length === -1) { 57 | parentElem.remove(); 58 | } 59 | }; 60 | }, 61 | [id], 62 | ); 63 | 64 | /** 65 | * It's important we evaluate this lazily: 66 | * - We need first render to contain the DOM element, so it shouldn't happen 67 | * in useEffect. We would normally put this in the constructor(). 68 | * - We can't do 'const rootElemRef = useRef(document.createElement('div))', 69 | * since this will run every single render (that's a lot). 70 | * - We want the ref to consistently point to the same DOM element and only 71 | * ever run once. 72 | * @link https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily 73 | */ 74 | function getRootElem() { 75 | if (!rootElemRef.current) { 76 | rootElemRef.current = document.createElement('div'); 77 | } 78 | return rootElemRef.current; 79 | } 80 | 81 | return getRootElem(); 82 | } 83 | 84 | export default usePortal; 85 | -------------------------------------------------------------------------------- /src/modules/modal/hooks/useModalControls.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Api { 4 | toggleModalState: VoidFunction; 5 | } 6 | 7 | export const useModalControls = () => { 8 | const [isModalOpen, setIsModalOpen] = React.useState(false); 9 | 10 | const toggleModalState = React.useCallback(() => { 11 | const newState = !isModalOpen; 12 | setIsModalOpen(newState); 13 | }, [isModalOpen]); 14 | 15 | const state = isModalOpen; 16 | const api = React.useMemo( 17 | () => ({ 18 | toggleModalState, 19 | }), 20 | [toggleModalState], 21 | ); 22 | 23 | return [state, api] as [boolean, Api]; 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/modal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './components'; 3 | -------------------------------------------------------------------------------- /src/modules/redux-store/helpers/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | 4 | import { activitiesReducer } from 'modules/activities'; 5 | import { appReducer } from 'modules/app'; 6 | import { contactsReducer } from 'modules/contacts'; 7 | import { scoreReducer } from 'modules/score'; 8 | import { settingsReducer } from 'modules/settings'; 9 | import { userReducer } from 'modules/user'; 10 | import { assistantReducer } from 'modules/assistant'; 11 | 12 | export const configureStore = () => { 13 | const rootReducer = { 14 | assistant: assistantReducer, 15 | user: userReducer, 16 | settings: settingsReducer, 17 | score: scoreReducer, 18 | activities: activitiesReducer, 19 | contacts: contactsReducer, 20 | app: appReducer, 21 | }; 22 | 23 | return process.env.NODE_ENV === 'production' 24 | ? createStore(combineReducers(rootReducer)) 25 | : createStore(combineReducers(rootReducer), {}, composeWithDevTools()); 26 | }; 27 | -------------------------------------------------------------------------------- /src/modules/redux-store/helpers/createAction.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionWithPayload } from '../models'; 2 | 3 | /** 4 | * Create action that dispatches only Type 5 | * Example: SomethingRequest 6 | */ 7 | export function createAction(type: T): Action; 8 | 9 | /** 10 | * Create action that dispatches type and payload 11 | * Example: SomethingSuccess | SomethingError 12 | */ 13 | export function createAction( 14 | type: T, 15 | payload: P, 16 | ): ActionWithPayload; 17 | 18 | /** 19 | * Create action that dispatches type and optional payload 20 | * This one is used for allowing resolution between the previous two as overloads 21 | */ 22 | export function createAction(type: T, payload?: P) { 23 | return !payload ? { type } : { type, payload }; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/redux-store/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createAction'; 2 | export * from './configureStore'; 3 | -------------------------------------------------------------------------------- /src/modules/redux-store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | export * from './helpers'; 3 | -------------------------------------------------------------------------------- /src/modules/redux-store/models/Action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Action that has only type property 3 | * Example: 4 | * { 5 | * type: SOME_ACTION 6 | * } 7 | */ 8 | export interface Action { 9 | type: T; 10 | } 11 | 12 | /** 13 | * Action that has both type and payload 14 | * Example: 15 | * { 16 | * type: SOME_ACTION 17 | * payload: { 18 | * user?: user 19 | * } 20 | * } 21 | */ 22 | export interface ActionWithPayload extends Action { 23 | payload: P; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/redux-store/models/ActionUnion.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreatorsMapObject } from 'redux'; 2 | 3 | /** This type is used for merging All actions of the same type 4 | * Example: 5 | * type RegistrationAction = 6 | * | RegistrationRequest 7 | * | RegistrationSuccess 8 | * | RegistrationError; 9 | */ 10 | export type ActionUnion = ReturnType< 11 | A[keyof A] 12 | >; 13 | -------------------------------------------------------------------------------- /src/modules/redux-store/models/ApplicationState.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from 'modules/app'; 2 | import { SettingsState } from 'modules/settings'; 3 | import { UserState } from 'modules/user'; 4 | import { ScoreState } from 'modules/score'; 5 | import { ActivitiesState } from 'modules/activities'; 6 | import { ContactsState } from 'modules/contacts'; 7 | import { AssistantState } from 'modules/assistant'; 8 | 9 | export interface ApplicationState { 10 | user: UserState; 11 | assistant: AssistantState; 12 | settings: SettingsState; 13 | score: ScoreState; 14 | activities: ActivitiesState; 15 | contacts: ContactsState; 16 | app: AppState; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/redux-store/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Action'; 2 | export * from './ActionUnion'; 3 | export * from './ApplicationState'; 4 | -------------------------------------------------------------------------------- /src/modules/routing/components/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import { Route, Redirect, RouteProps } from 'react-router-dom'; 4 | import { useIsOnline } from 'react-use-is-online'; 5 | 6 | import { useUserServices } from 'modules/user'; 7 | import { SplashScreen, Offline } from 'components'; 8 | 9 | const PrivateRoute: React.FC = (props) => { 10 | const [{ userData, isLoading }] = useUserServices(); 11 | const { isOffline } = useIsOnline(); 12 | const shouldDisplaySplashScreen = React.useMemo( 13 | () => !!userData?.uid && isLoading, 14 | [isLoading, userData], 15 | ); 16 | 17 | if (isOffline) return ; 18 | 19 | if (shouldDisplaySplashScreen) return ; 20 | 21 | return isEmpty(userData) ? : ; 22 | }; 23 | 24 | export { PrivateRoute }; 25 | -------------------------------------------------------------------------------- /src/modules/routing/components/PublicRoute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { isEmpty } from 'lodash'; 3 | import { Route, Redirect, RouteProps } from 'react-router-dom'; 4 | import { useIsOnline } from 'react-use-is-online'; 5 | 6 | import { useUserServices } from 'modules/user'; 7 | import { SplashScreen, Offline } from 'components'; 8 | 9 | const PublicRoute: React.FC = (props) => { 10 | const [{ userData, isLoading }] = useUserServices(); 11 | const { isOffline } = useIsOnline(); 12 | const [ready, setIsReady] = React.useState(false); 13 | 14 | React.useEffect(() => { 15 | setTimeout(() => setIsReady(true), 2000); 16 | }, []); 17 | 18 | if (isOffline) return ; 19 | 20 | if ((!userData && isLoading) || !ready) return ; 21 | 22 | return isEmpty(userData) ? : ; 23 | }; 24 | 25 | export { PublicRoute }; 26 | -------------------------------------------------------------------------------- /src/modules/routing/components/Routing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import { PrivateRoute, PublicRoute } from 'modules/routing'; 5 | import { 6 | Dashboard, 7 | Login, 8 | SignUp, 9 | Welcome, 10 | Contacts, 11 | Activities, 12 | Profile, 13 | Assistant, 14 | NotFound, 15 | } from 'views'; 16 | import { useAuthData } from 'modules/user'; 17 | 18 | export const Routing: React.FC = () => { 19 | useAuthData(); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/modules/routing/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Routing'; 2 | export * from './PrivateRoute'; 3 | export * from './PublicRoute'; 4 | -------------------------------------------------------------------------------- /src/modules/routing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /src/modules/score/components/ScoreTracker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; 3 | 4 | import { ReactComponent as ScoreTrackerDeco } from 'assets/icons/circle_star.svg'; 5 | import { useScoreServices } from 'modules/score'; 6 | import { Heading } from 'components'; 7 | 8 | const SVG_STYLES = { 9 | rotation: 0.6, 10 | strokeLinecap: 'round', 11 | pathTransitionDuration: 0.5, 12 | pathColor: `#FF3C3C`, 13 | trailColor: 'rgba(48,53,72,0.1)', 14 | }; 15 | 16 | interface Props { 17 | mode: 'small' | 'large'; 18 | } 19 | 20 | export const ScoreTracker = ({ mode }: Props) => { 21 | const [{ userScore }] = useScoreServices(); 22 | 23 | const score = userScore ? userScore.score : 0; 24 | 25 | const isLarge = mode === 'large'; 26 | 27 | const getClassName = (component: string) => 28 | isLarge 29 | ? `scoretracker__${component} scoretracker__${component}--large` 30 | : `scoretracker__${component}`; 31 | 32 | const fontSize = isLarge ? 'u-t__fontSize--xlarge' : 'u-t__fontSize--base'; 33 | const mainClass = isLarge 34 | ? 'scoretracker scoretracker--large u-ab-center' 35 | : 'scoretracker'; 36 | 37 | const textClass = getClassName('text'); 38 | const decoClass = getClassName('deco'); 39 | 40 | const valueClass = getClassName('value'); 41 | return ( 42 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/modules/score/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ScoreTracker'; 2 | -------------------------------------------------------------------------------- /src/modules/score/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useScoreListener'; 2 | export * from './useScoreServices'; 3 | -------------------------------------------------------------------------------- /src/modules/score/hooks/useScoreListener.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import * as React from 'react'; 3 | 4 | import { getUserScoreData, ScoreActionTypes } from 'modules/score'; 5 | 6 | export const useScoreListener = (user?: firebase.UserInfo) => { 7 | const dispatch = useDispatch(); 8 | 9 | const successFunction = React.useCallback( 10 | (payload) => { 11 | dispatch({ 12 | type: ScoreActionTypes.Success, 13 | payload, 14 | }); 15 | }, 16 | [dispatch], 17 | ); 18 | 19 | const errorFunction = React.useCallback( 20 | (error) => { 21 | dispatch({ 22 | type: ScoreActionTypes.Error, 23 | error, 24 | }); 25 | }, 26 | [dispatch], 27 | ); 28 | 29 | React.useEffect(() => { 30 | if (user && user.uid) { 31 | dispatch({ type: ScoreActionTypes.Request }); 32 | getUserScoreData(user, successFunction, errorFunction); 33 | } 34 | }, [dispatch, errorFunction, successFunction, user]); 35 | }; 36 | -------------------------------------------------------------------------------- /src/modules/score/hooks/useScoreServices.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import * as React from 'react'; 3 | 4 | import { getUserData } from 'modules/user'; 5 | import { CustomHook } from 'models'; 6 | import { 7 | getUserScoreHistoryData, 8 | ScoreActionTypes, 9 | getUserScore, 10 | ScoreState, 11 | } from 'modules/score'; 12 | 13 | interface Api { 14 | getScoreHistory: VoidFunction; 15 | } 16 | 17 | export const useScoreServices: CustomHook = () => { 18 | const dispatch = useDispatch(); 19 | const { userData } = useSelector(getUserData); 20 | const score = useSelector(getUserScore); 21 | 22 | const getScoreHistory = React.useCallback(async () => { 23 | if (!userData) return; 24 | dispatch({ 25 | type: ScoreActionTypes.Request, 26 | }); 27 | 28 | const history = await getUserScoreHistoryData(userData); 29 | 30 | dispatch({ 31 | type: ScoreActionTypes.Success, 32 | payload: { history }, 33 | }); 34 | }, [dispatch, userData]); 35 | 36 | const api = { getScoreHistory }; 37 | 38 | const state = score; 39 | 40 | return [state, api]; 41 | }; 42 | -------------------------------------------------------------------------------- /src/modules/score/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redux'; 2 | export * from './components'; 3 | export * from './services'; 4 | export * from './hooks'; 5 | export * from './models'; 6 | -------------------------------------------------------------------------------- /src/modules/score/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/score/models/models.ts: -------------------------------------------------------------------------------- 1 | import { UserActivity } from 'modules/activities'; 2 | export type ScoreHistory = { 3 | date: Date; 4 | title: string; 5 | score: number; 6 | style: string; 7 | }; 8 | 9 | export type UserScore = { 10 | score: number; 11 | history: UserActivity[]; 12 | }; 13 | 14 | export interface ScoreState { 15 | isLoading: boolean; 16 | userScore?: UserScore; 17 | error?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/score/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { ScoreActionTypes, UserScore } from 'modules/score'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const SettingsActions = { 5 | Request: () => createAction(ScoreActionTypes.Request), 6 | 7 | Success: (settings: UserScore) => 8 | createAction(ScoreActionTypes.Success, settings), 9 | 10 | Error: (error?: string) => createAction(ScoreActionTypes.Error, error), 11 | }; 12 | 13 | export type ScoreActions = ActionUnion; 14 | -------------------------------------------------------------------------------- /src/modules/score/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/score/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ScoreActionTypes, ScoreActions, ScoreState } from 'modules/score'; 2 | 3 | const INITIAL_STATE: ScoreState = { 4 | userScore: undefined, 5 | isLoading: false, 6 | error: undefined, 7 | }; 8 | 9 | export const scoreReducer = ( 10 | state: ScoreState = INITIAL_STATE, 11 | action: ScoreActions, 12 | ): ScoreState => { 13 | switch (action.type) { 14 | case ScoreActionTypes.Request: 15 | return { 16 | ...state, 17 | isLoading: true, 18 | error: undefined, 19 | }; 20 | case ScoreActionTypes.Success: 21 | return { 22 | ...state, 23 | userScore: { ...state.userScore, ...action.payload }, 24 | isLoading: false, 25 | }; 26 | case ScoreActionTypes.Error: 27 | return { 28 | ...state, 29 | error: action.payload, 30 | isLoading: false, 31 | }; 32 | default: 33 | return state || INITIAL_STATE; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/modules/score/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getUserScore = ({ score }: ApplicationState) => score; 4 | -------------------------------------------------------------------------------- /src/modules/score/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetScoreActionTypes { 2 | Request = 'SCORES_DATA_REQUEST', 3 | Success = 'SCORES_DATA_SUCCESS', 4 | Error = 'SCORES_DATA_ERROR', 5 | } 6 | 7 | export const ScoreActionTypes = { 8 | ...GetScoreActionTypes, 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/score/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './updateScoreHistory'; 2 | -------------------------------------------------------------------------------- /src/modules/score/services/updateScoreHistory.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreService } from 'modules/firebase'; 2 | import { ScoreHistory } from 'modules/score'; 3 | 4 | const updateScoreHistory = async ( 5 | user: firebase.UserInfo, 6 | value: ScoreHistory, 7 | ) => { 8 | const firestore = new FirestoreService(`score/${user.uid}/history`); 9 | 10 | firestore.addAsync(value); 11 | }; 12 | 13 | const getUserScoreHistoryData = async (user: firebase.UserInfo) => { 14 | const firestore = new FirestoreService(`score`); 15 | return firestore.getSubcollection(user.uid, 'history'); 16 | }; 17 | 18 | const getUserScoreData = ( 19 | user: firebase.UserInfo, 20 | successFunction: (data: unknown) => void, 21 | errorFunction: (error: string) => void, 22 | ) => { 23 | const firestore = new FirestoreService(`score`); 24 | 25 | firestore.getByIdAsync(user.uid, { 26 | successFunction, 27 | errorFunction, 28 | }); 29 | }; 30 | 31 | export { getUserScoreData, updateScoreHistory, getUserScoreHistoryData }; 32 | -------------------------------------------------------------------------------- /src/modules/settings/components/SplashQuestion.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ReactComponent as Mascot } from 'assets/icons/mascot.svg'; 4 | import { ReactComponent as Mascots } from 'assets/icons/mascot_x3.svg'; 5 | import { Button, BUTTON, HEADING, Heading } from 'components'; 6 | 7 | interface Props { 8 | question: string; 9 | handleOnClick: (e: React.MouseEvent) => void; 10 | answerNegative: string; 11 | answerPositive: string; 12 | questionNum: number; 13 | questionMax: number; 14 | } 15 | 16 | const SplashQuestion: React.FC = ({ 17 | question, 18 | handleOnClick, 19 | answerNegative, 20 | answerPositive, 21 | questionNum, 22 | questionMax, 23 | }) => { 24 | return ( 25 | <> 26 |
    27 |
    28 | 29 | Question {questionNum}{' '} 30 | of {questionMax} 31 | 32 |
    33 |
    34 | 35 | {question} 36 | 37 |
    38 | {questionNum === questionMax ? ( 39 | 40 | ) : ( 41 | 42 | )} 43 |
    44 |
    45 | 52 | 59 |
    60 | 61 | ); 62 | }; 63 | 64 | export { SplashQuestion }; 65 | -------------------------------------------------------------------------------- /src/modules/settings/components/SplashSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import { 5 | SplashQuestion, 6 | QUESTIONS, 7 | useSettingsServices, 8 | } from 'modules/settings'; 9 | import { useUserServices } from 'modules/user'; 10 | import { useAppState } from 'modules/app'; 11 | import { SplashScreen } from 'components'; 12 | 13 | const SplashSettings: React.FC = () => { 14 | const COLORS = React.useMemo(() => ['#6A62FF', '#F85E5E', '#FAC936'], []); 15 | const [{ userSettings }, { updateSettings }] = useSettingsServices(); 16 | const [{ userData }] = useUserServices(); 17 | const [, { setAppTheme }] = useAppState(); 18 | const [questionNum, setQuestionNum] = React.useState(0); 19 | 20 | React.useEffect(() => { 21 | setAppTheme({ 22 | color: COLORS[questionNum], 23 | shapeClass: 'app__deco--default', 24 | showNav: false, 25 | }); 26 | }, [COLORS, questionNum, setAppTheme]); 27 | 28 | if (userSettings && userSettings.surveyCompleted) return ; 29 | 30 | if (questionNum >= QUESTIONS.length) return ; 31 | 32 | const { label } = QUESTIONS[questionNum]; 33 | 34 | const handleOnClick = async (e: React.MouseEvent) => { 35 | if (!userData) return; 36 | const { value } = e.currentTarget; 37 | const isLastQuestion = questionNum + 1 >= QUESTIONS.length; 38 | 39 | const updatedValue = { 40 | [label]: value === 'true', 41 | surveyCompleted: isLastQuestion, 42 | }; 43 | 44 | updateSettings(userData, updatedValue); 45 | setQuestionNum(questionNum + 1); 46 | }; 47 | 48 | return ( 49 | 55 | ); 56 | }; 57 | 58 | export { SplashSettings }; 59 | -------------------------------------------------------------------------------- /src/modules/settings/components/UpdateSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | import { ReactComponent as Checkbox } from 'assets/icons/checkbox.svg'; 5 | import { QUESTIONS, useSettingsServices } from 'modules/settings'; 6 | import { useUserServices } from 'modules/user'; 7 | 8 | export const UpdateSettings: React.FC = () => { 9 | const [{ userData }] = useUserServices(); 10 | const [ 11 | { userSettings, isLoading }, 12 | { updateSettings }, 13 | ] = useSettingsServices(); 14 | const { register } = useForm({ 15 | defaultValues: { 16 | hasAssignedSelfIsolation: userSettings?.hasAssignedSelfIsolation 17 | ? '1' 18 | : '0', 19 | isLivingAlone: userSettings?.isLivingAlone ? '1' : '0', 20 | }, 21 | }); 22 | 23 | const onChange = React.useCallback( 24 | (e: React.ChangeEvent) => { 25 | const { name, value } = e.currentTarget; 26 | if (!userData) return; 27 | updateSettings(userData, { 28 | [name]: !!parseInt(value), 29 | }); 30 | }, 31 | [updateSettings, userData], 32 | ); 33 | 34 | return ( 35 |
    36 | {QUESTIONS.map( 37 | ({ question, answerPositive, answerNegative, label }, index) => ( 38 |
    39 |
    40 | Question {index + 1} 41 |
    42 | 43 | {question} 44 | 45 |
    46 | 55 | 62 |
    63 |
    64 | 73 | 80 |
    81 |
    82 | ), 83 | )} 84 |
    85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/modules/settings/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SplashSettings'; 2 | export * from './SplashQuestion'; 3 | export * from './UpdateSettings'; 4 | -------------------------------------------------------------------------------- /src/modules/settings/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './questions'; 2 | -------------------------------------------------------------------------------- /src/modules/settings/constants/questions.ts: -------------------------------------------------------------------------------- 1 | export const QUESTIONS = [ 2 | { 3 | question: 'Are you currently assigned a mandatory self-isolation?', 4 | answerPositive: 'Yup, gotta stay indoors', 5 | answerNegative: "Nope, but stayin' inside all the same", 6 | label: 'hasAssignedSelfIsolation', 7 | }, 8 | { 9 | question: 10 | 'Do you live by yourself or are there more people in your household?', 11 | answerPositive: "Livin' alone", 12 | answerNegative: 'Oh no, there are more of us', 13 | label: 'isLivingAlone', 14 | }, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/modules/settings/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useSettingsServices'; 2 | -------------------------------------------------------------------------------- /src/modules/settings/hooks/useSettingsServices.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { CustomHook } from 'models'; 5 | import { 6 | getUserDocumentSettings, 7 | updateUserSettings, 8 | SettingsActionTypes, 9 | getUserSettings, 10 | SettingsState, 11 | } from 'modules/settings'; 12 | 13 | interface Api { 14 | getSettings: (userData: firebase.UserInfo) => void; 15 | resetSettings: VoidFunction; 16 | updateSettings: ( 17 | userData: firebase.UserInfo, 18 | updatedValue: { [key: string]: boolean }, 19 | ) => Promise; 20 | } 21 | 22 | export const useSettingsServices: CustomHook = () => { 23 | const dispatch = useDispatch(); 24 | const settings = useSelector(getUserSettings); 25 | 26 | const resetSettings = React.useCallback(() => { 27 | dispatch({ type: SettingsActionTypes.Reset }); 28 | }, [dispatch]); 29 | 30 | const getSettings = React.useCallback( 31 | async (userData?: firebase.UserInfo) => { 32 | if (!userData) return; 33 | dispatch({ type: SettingsActionTypes.Request }); 34 | const updatedSettings = await getUserDocumentSettings(userData); 35 | dispatch({ type: SettingsActionTypes.Success, payload: updatedSettings }); 36 | }, 37 | [dispatch], 38 | ); 39 | 40 | const updateSettings = React.useCallback( 41 | async ( 42 | userData?: firebase.UserInfo, 43 | updatedValue?: { [key: string]: boolean }, 44 | ) => { 45 | if (!userData) return; 46 | await updateUserSettings(userData, updatedValue); 47 | getSettings(userData); 48 | }, 49 | [getSettings], 50 | ); 51 | 52 | const api = React.useMemo( 53 | () => ({ 54 | getSettings, 55 | updateSettings, 56 | resetSettings, 57 | }), 58 | [getSettings, resetSettings, updateSettings], 59 | ); 60 | 61 | const state = settings; 62 | 63 | return [state, api]; 64 | }; 65 | -------------------------------------------------------------------------------- /src/modules/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './services'; 3 | export * from './redux'; 4 | export * from './constants'; 5 | export * from './hooks'; 6 | export * from './models'; 7 | -------------------------------------------------------------------------------- /src/modules/settings/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/settings/models/models.ts: -------------------------------------------------------------------------------- 1 | export type UserSettings = { [key: string]: boolean }; 2 | 3 | export interface SettingsState { 4 | isLoading: boolean; 5 | userSettings?: UserSettings; 6 | error?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/settings/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { SettingsActionTypes, UserSettings } from 'modules/settings'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const SettingsActions = { 5 | Request: () => createAction(SettingsActionTypes.Request), 6 | 7 | Success: (settings: UserSettings) => 8 | createAction(SettingsActionTypes.Success, settings), 9 | 10 | Error: (error?: string) => createAction(SettingsActionTypes.Error, error), 11 | Reset: () => createAction(SettingsActionTypes.Reset), 12 | }; 13 | 14 | export type SettingsActions = ActionUnion; 15 | -------------------------------------------------------------------------------- /src/modules/settings/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/settings/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SettingsActionTypes, 3 | SettingsActions, 4 | SettingsState, 5 | } from 'modules/settings'; 6 | 7 | const INITIAL_STATE: SettingsState = { 8 | userSettings: undefined, 9 | isLoading: false, 10 | error: undefined, 11 | }; 12 | 13 | export const settingsReducer = ( 14 | state: SettingsState = INITIAL_STATE, 15 | action: SettingsActions, 16 | ): SettingsState => { 17 | switch (action.type) { 18 | case SettingsActionTypes.Request: 19 | return { 20 | ...state, 21 | isLoading: true, 22 | error: undefined, 23 | }; 24 | case SettingsActionTypes.Success: 25 | return { 26 | ...state, 27 | userSettings: action.payload, 28 | isLoading: false, 29 | }; 30 | case SettingsActionTypes.Error: 31 | return { 32 | ...state, 33 | error: action.payload, 34 | isLoading: false, 35 | }; 36 | case SettingsActionTypes.Reset: 37 | return INITIAL_STATE; 38 | default: 39 | return state || INITIAL_STATE; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/modules/settings/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getUserSettings = ({ settings }: ApplicationState) => settings; 4 | -------------------------------------------------------------------------------- /src/modules/settings/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetSettingsActionTypes { 2 | Request = 'SETTINGS_DATA_REQUEST', 3 | Success = 'SETTINGS_DATA_SUCCESS', 4 | Error = 'SETTINGS_DATA_ERROR', 5 | Reset = 'SETTINGS_DATA_RESET', 6 | } 7 | 8 | export const SettingsActionTypes = { 9 | ...GetSettingsActionTypes, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/settings/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './updateUserSettings'; 2 | -------------------------------------------------------------------------------- /src/modules/settings/services/updateUserSettings.ts: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty'; 2 | 3 | import { FirestoreService } from 'modules/firebase'; 4 | 5 | export const getUserDocumentSettings = async (user?: firebase.UserInfo) => { 6 | if (isEmpty(user) || !user) return null; 7 | const firestore = new FirestoreService('settings'); 8 | return firestore.getByIdAsync(user.uid); 9 | }; 10 | 11 | const updateUserSettings = async ( 12 | user?: firebase.UserInfo, 13 | value?: { [key: string]: boolean }, 14 | ) => { 15 | if (!user || !value || isEmpty(user) || isEmpty(value)) return; 16 | const firestore = new FirestoreService('settings'); 17 | const settings = await getUserDocumentSettings(user); 18 | 19 | delete settings.id; 20 | 21 | if (isEmpty(settings)) { 22 | firestore.addAsync({ id: user.uid, ...value }); 23 | } else { 24 | firestore.updateByIdAsync(value, user.uid); 25 | } 26 | }; 27 | 28 | export { updateUserSettings }; 29 | -------------------------------------------------------------------------------- /src/modules/user/components/LogOut.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FirebaseService } from 'modules/firebase'; 4 | import { Button, BUTTON } from 'components'; 5 | 6 | const LogOut: React.FC = () => { 7 | const auth = FirebaseService.AuthProvider; 8 | 9 | const handleLogout = React.useCallback(() => { 10 | auth.signOut(); 11 | }, [auth]); 12 | 13 | return ( 14 | 17 | ); 18 | }; 19 | 20 | export { LogOut }; 21 | -------------------------------------------------------------------------------- /src/modules/user/components/LoginWithEmail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useForm, FieldValues } from 'react-hook-form'; 3 | import { toast, ToastType, ToastPosition } from 'react-toastify'; 4 | 5 | import { emailRegex } from 'util/validation'; 6 | import { FirebaseService } from 'modules/firebase'; 7 | import { TextInput, Button, BUTTON } from 'components'; 8 | 9 | const LoginWithEmail: React.FC = () => { 10 | const { handleSubmit, register, errors, watch } = useForm(); 11 | const authProvider = FirebaseService.AuthProvider; 12 | 13 | const { email, password } = watch(); 14 | 15 | const onSubmit = async (values: FieldValues) => { 16 | const { email, password } = values; 17 | try { 18 | await authProvider.signInWithEmailAndPassword(email, password); 19 | } catch (error) { 20 | toast(error.message, { 21 | closeButton: false, 22 | position: ToastPosition.TOP_CENTER, 23 | type: ToastType.ERROR, 24 | }); 25 | } 26 | }; 27 | 28 | return ( 29 |
    30 |
    31 | 45 | 46 | 55 |
    56 | 57 | 58 |
    59 | ); 60 | }; 61 | 62 | export { LoginWithEmail }; 63 | -------------------------------------------------------------------------------- /src/modules/user/components/LoginWithGoogle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FirebaseService } from 'modules/firebase'; 4 | import { Button, BUTTON } from 'components'; 5 | import { ReactComponent as GoogleIcon } from 'assets/icons/google.svg'; 6 | 7 | const LoginWithGoogle: React.FC = ({ children }) => { 8 | const firebase = FirebaseService.Instance; 9 | const authGoogle = FirebaseService.AuthProviderGoogle; 10 | const authProvider = firebase.auth(); 11 | 12 | const handleLoginGoogle = React.useCallback(() => { 13 | authProvider.signInWithPopup(authGoogle); 14 | }, [authGoogle, authProvider]); 15 | 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export { LoginWithGoogle }; 28 | -------------------------------------------------------------------------------- /src/modules/user/components/SignupEmail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toast, ToastPosition, ToastType } from 'react-toastify'; 3 | import { useForm, FieldValues } from 'react-hook-form'; 4 | 5 | import { emailRegex } from 'util/validation'; 6 | import { FirebaseService } from 'modules/firebase'; 7 | import { createUserDocument } from 'modules/user'; 8 | import { TextInput, BUTTON, Button } from 'components'; 9 | 10 | const SignUpEmail: React.FC = () => { 11 | const { handleSubmit, register, errors, watch } = useForm(); 12 | const authProvider = FirebaseService.AuthProvider; 13 | 14 | const { email, password } = watch(); 15 | 16 | const onSubmit = async (values: FieldValues) => { 17 | const { email, password } = values; 18 | try { 19 | const { user } = await authProvider.createUserWithEmailAndPassword( 20 | email, 21 | password, 22 | ); 23 | if (!user) return; 24 | await createUserDocument(user); 25 | } catch (error) { 26 | toast(error.message, { 27 | closeButton: false, 28 | position: ToastPosition.TOP_CENTER, 29 | type: ToastType.ERROR, 30 | }); 31 | } 32 | }; 33 | 34 | const emailPattern = { 35 | value: emailRegex, 36 | message: 'Invalid email address', 37 | }; 38 | 39 | const emailRef = register({ 40 | required: 'This field is required', 41 | pattern: emailPattern, 42 | }); 43 | 44 | const passwordRef = register({ 45 | required: 'This field is required', 46 | min: { value: 6, message: 'Password should be at least 6 characters' }, 47 | }); 48 | 49 | return ( 50 |
    51 |
    52 | 60 | 61 | 70 |
    71 | 72 |
    73 | 74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export { SignUpEmail }; 80 | -------------------------------------------------------------------------------- /src/modules/user/components/UpdateProfile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm, FieldValues } from 'react-hook-form'; 3 | import { toast, ToastPosition, ToastType } from 'react-toastify'; 4 | 5 | import { TextInput, Button, BUTTON } from 'components'; 6 | import { useUserServices } from 'modules/user'; 7 | 8 | export const UpdateProfile = () => { 9 | const [{ userData }, { updateUserProfile }] = useUserServices(); 10 | const { handleSubmit, register, errors, watch } = useForm({ 11 | defaultValues: { 12 | displayName: userData?.displayName, 13 | }, 14 | }); 15 | const { displayName } = watch(); 16 | 17 | const onSubmit = React.useCallback( 18 | async (values: FieldValues) => { 19 | const { displayName } = values; 20 | if (!userData || userData.displayName === displayName) return; 21 | try { 22 | await updateUserProfile(userData, { displayName }); 23 | toast('Profile saved successfuly', { 24 | closeButton: false, 25 | position: ToastPosition.TOP_CENTER, 26 | type: ToastType.SUCCESS, 27 | }); 28 | } catch (error) { 29 | toast(error.message, { 30 | closeButton: false, 31 | position: ToastPosition.TOP_CENTER, 32 | type: ToastType.ERROR, 33 | }); 34 | } 35 | }, 36 | [updateUserProfile, userData], 37 | ); 38 | 39 | return ( 40 |
    41 |
    42 | 52 |
    53 | 54 |
    55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/modules/user/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LoginWithGoogle'; 2 | export * from './LogOut'; 3 | export * from './LoginWithEmail'; 4 | export * from './SignupEmail'; 5 | export * from './UpdateProfile'; 6 | -------------------------------------------------------------------------------- /src/modules/user/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAuthData'; 2 | export * from './useUserServices'; 3 | export * from './useAuthStateChange'; 4 | -------------------------------------------------------------------------------- /src/modules/user/hooks/useAuthData.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import { FirebaseService } from 'modules/firebase'; 4 | import { useAuthStateChange } from 'modules/user'; 5 | 6 | const useAuthData = () => { 7 | const handleAuthChange = useAuthStateChange(); 8 | const unsubscribeFromAuth = useRef(() => false); 9 | 10 | useEffect(() => { 11 | const auth = FirebaseService.AuthProvider; 12 | unsubscribeFromAuth.current = auth.onAuthStateChanged(handleAuthChange); 13 | 14 | return () => { 15 | unsubscribeFromAuth.current(); 16 | }; 17 | }, [handleAuthChange]); 18 | }; 19 | 20 | export { useAuthData }; 21 | -------------------------------------------------------------------------------- /src/modules/user/hooks/useAuthStateChange.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useUserServices } from 'modules/user'; 4 | import { useSettingsServices } from 'modules/settings'; 5 | 6 | export const useAuthStateChange = () => { 7 | const [, api] = useSettingsServices(); 8 | const [, { updateUserData, resetUserData }] = useUserServices(); 9 | 10 | const { getSettings, resetSettings } = api; 11 | 12 | const handleAuthChange = useCallback( 13 | async (userAuth) => { 14 | if (userAuth) { 15 | updateUserData(userAuth); 16 | getSettings(userAuth); 17 | return; 18 | } 19 | 20 | resetSettings(); 21 | resetUserData(); 22 | }, 23 | [getSettings, resetSettings, resetUserData, updateUserData], 24 | ); 25 | 26 | return handleAuthChange; 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/user/hooks/useUserServices.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { CustomHook } from 'models'; 5 | import { 6 | getUserData, 7 | UserActionTypes, 8 | createUserDocument, 9 | UserState, 10 | updateUserDocument, 11 | } from 'modules/user'; 12 | 13 | interface Api { 14 | updateUserData: (user: firebase.UserInfo) => void; 15 | resetUserData: VoidFunction; 16 | updateUserProfile: ( 17 | user: Partial, 18 | value: Partial, 19 | ) => void; 20 | } 21 | 22 | export const useUserServices: CustomHook = () => { 23 | const dispatch = useDispatch(); 24 | 25 | const user = useSelector(getUserData); 26 | 27 | const updateUserProfile = React.useCallback( 28 | async (userData, value) => { 29 | const payload = await updateUserDocument(userData, value); 30 | dispatch({ 31 | type: UserActionTypes.Success, 32 | payload, 33 | }); 34 | }, 35 | [dispatch], 36 | ); 37 | 38 | const updateUserData = React.useCallback( 39 | async (userData) => { 40 | dispatch({ type: UserActionTypes.Request }); 41 | const payload = await createUserDocument(userData); 42 | dispatch({ 43 | type: UserActionTypes.Success, 44 | payload, 45 | }); 46 | }, 47 | [dispatch], 48 | ); 49 | 50 | const resetUserData = React.useCallback(() => { 51 | dispatch({ type: UserActionTypes.Reset }); 52 | }, [dispatch]); 53 | 54 | const state = user; 55 | 56 | const api = React.useMemo( 57 | () => ({ 58 | updateUserProfile, 59 | updateUserData, 60 | resetUserData, 61 | }), 62 | [resetUserData, updateUserData, updateUserProfile], 63 | ); 64 | 65 | return [state, api]; 66 | }; 67 | -------------------------------------------------------------------------------- /src/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './services'; 3 | export * from './models'; 4 | export * from './redux'; 5 | export * from './hooks'; 6 | -------------------------------------------------------------------------------- /src/modules/user/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | -------------------------------------------------------------------------------- /src/modules/user/models/models.ts: -------------------------------------------------------------------------------- 1 | export interface UserAuthData { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export interface UserState { 7 | isLoading: boolean; 8 | userData?: firebase.UserInfo; 9 | error?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/user/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { UserActionTypes } from 'modules/user'; 2 | import { ActionUnion, createAction } from 'modules/redux-store'; 3 | 4 | export const UserActions = { 5 | Request: () => createAction(UserActionTypes.Request), 6 | 7 | Success: (userData: firebase.User) => 8 | createAction(UserActionTypes.Success, userData), 9 | 10 | Error: (error?: string) => createAction(UserActionTypes.Error, error), 11 | Reset: () => createAction(UserActionTypes.Reset), 12 | }; 13 | 14 | export type UserActions = ActionUnion; 15 | -------------------------------------------------------------------------------- /src/modules/user/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './actions'; 3 | export * from './types'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/user/redux/reducer.ts: -------------------------------------------------------------------------------- 1 | import { UserActions, UserActionTypes, UserState } from 'modules/user'; 2 | 3 | const INITIAL_STATE: UserState = { 4 | userData: undefined, 5 | isLoading: false, 6 | error: undefined, 7 | }; 8 | 9 | export const userReducer = ( 10 | state: UserState = INITIAL_STATE, 11 | action: UserActions, 12 | ): UserState => { 13 | switch (action.type) { 14 | case UserActionTypes.Request: 15 | return { 16 | ...state, 17 | isLoading: true, 18 | error: undefined, 19 | }; 20 | case UserActionTypes.Success: 21 | return { 22 | ...state, 23 | userData: action.payload, 24 | isLoading: false, 25 | }; 26 | case UserActionTypes.Error: 27 | return { 28 | ...state, 29 | error: action.payload, 30 | isLoading: false, 31 | }; 32 | case UserActionTypes.Reset: 33 | return INITIAL_STATE; 34 | default: 35 | return state || INITIAL_STATE; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/modules/user/redux/selectors.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationState } from 'modules/redux-store'; 2 | 3 | export const getUserData = ({ user }: ApplicationState) => user; 4 | -------------------------------------------------------------------------------- /src/modules/user/redux/types.ts: -------------------------------------------------------------------------------- 1 | enum GetUserActionTypes { 2 | Request = 'USER_DATA_REQUEST', 3 | Success = 'USER_DATA_SUCCESS', 4 | Error = 'USER_DATA_ERROR', 5 | Reset = 'USER_DATA_RESET', 6 | } 7 | 8 | export const UserActionTypes = { 9 | ...GetUserActionTypes, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/user/services/createUserDocument.ts: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty'; 2 | 3 | import { FirestoreService } from 'modules/firebase'; 4 | 5 | const formatUserDocumentData = (user: firebase.UserInfo) => { 6 | const { displayName, email, photoURL, uid } = user; 7 | const createdAt = new Date(); 8 | 9 | return { 10 | id: uid, 11 | uid: uid, 12 | displayName, 13 | email, 14 | photoURL, 15 | createdAt, 16 | }; 17 | }; 18 | 19 | const getUserDocument = async (uid: firebase.UserInfo['uid']) => { 20 | if (!uid) return null; 21 | const firestore = new FirestoreService('users'); 22 | 23 | return firestore.getByIdAsync(uid); 24 | }; 25 | 26 | const updateUserDocument = async ( 27 | user: firebase.UserInfo, 28 | value: Partial, 29 | ) => { 30 | const firestore = new FirestoreService('users'); 31 | await firestore.updateByIdAsync(value, user.uid); 32 | const userData = await getUserDocument(user.uid); 33 | return userData; 34 | }; 35 | 36 | const createUserDocument = async ( 37 | user: firebase.UserInfo, 38 | additionalData?: Partial, 39 | ) => { 40 | if (isEmpty(user)) return; 41 | const userStore = new FirestoreService('users'); 42 | const settingsStore = new FirestoreService('settings'); 43 | const scoreStore = new FirestoreService('score'); 44 | const userRef = await getUserDocument(user.uid); 45 | 46 | if (userRef.uid) return userRef; 47 | 48 | const userData = formatUserDocumentData(user); 49 | 50 | userStore.addAsync({ 51 | ...userData, 52 | ...additionalData, 53 | }); 54 | 55 | settingsStore.addAsync({ 56 | id: user.uid, 57 | surveyCompleted: false, 58 | }); 59 | 60 | scoreStore.addAsync({ 61 | id: user.uid, 62 | score: 0, 63 | }); 64 | 65 | const updatedDocument = await getUserDocument(user.uid); 66 | 67 | return updatedDocument; 68 | }; 69 | 70 | export { createUserDocument, getUserDocument, updateUserDocument }; 71 | -------------------------------------------------------------------------------- /src/modules/user/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createUserDocument'; 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validation'; 2 | export * from './time'; 3 | -------------------------------------------------------------------------------- /src/util/mapping.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | import { toDate } from 'util/time'; 4 | import { UserContact } from 'modules/contacts'; 5 | import { GroupedContacts } from 'views'; 6 | 7 | export const getGroupedContacts = (userContacts: UserContact[]) => { 8 | const groupedContacts: GroupedContacts = {}; 9 | 10 | userContacts.forEach((contact) => { 11 | const newDate = format(toDate(contact.date), '-YMMdd'); 12 | 13 | if (!groupedContacts[newDate]) { 14 | groupedContacts[newDate] = { 15 | date: toDate(contact.date), 16 | contacts: [contact], 17 | }; 18 | } else { 19 | groupedContacts[newDate].contacts = [ 20 | contact, 21 | ...groupedContacts[newDate].contacts, 22 | ]; 23 | } 24 | }); 25 | 26 | return groupedContacts; 27 | }; 28 | -------------------------------------------------------------------------------- /src/util/time.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | export type Timestamp = { 4 | seconds: number; 5 | nanoseconds: number; 6 | }; 7 | 8 | export const toDate = ({ seconds, nanoseconds }: Timestamp) => 9 | new firebase.firestore.Timestamp(seconds, nanoseconds).toDate(); 10 | -------------------------------------------------------------------------------- /src/util/validation.ts: -------------------------------------------------------------------------------- 1 | export const emailRegex = /^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,9})$/i; 2 | -------------------------------------------------------------------------------- /src/views/Activities.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Heading, HEADING, Button, BUTTON, Tabs } from 'components'; 4 | import { ReactComponent as PlusIcon } from 'assets/icons/plus.svg'; 5 | import { useAppState } from 'modules/app'; 6 | import { 7 | CompletedActivity, 8 | ActivityList, 9 | useActivitiesServices, 10 | ActivityModal, 11 | } from 'modules/activities'; 12 | import { useModalControls } from 'modules/modal'; 13 | 14 | export const Activities = () => { 15 | const [, { getActivities }] = useActivitiesServices(); 16 | const [isModalOpen, { toggleModalState }] = useModalControls(); 17 | const [, { setAppTheme }] = useAppState(); 18 | 19 | React.useEffect(() => { 20 | getActivities(); 21 | }, [getActivities]); 22 | 23 | React.useEffect(() => { 24 | setAppTheme({ 25 | color: '#6A62FF', 26 | shapeClass: 'app__deco--default', 27 | showNav: true, 28 | }); 29 | }, [setAppTheme]); 30 | 31 | return ( 32 |
    33 | 43 |
    44 | 49 | } 51 | contentSecondary={} 52 | titleMain="Your todo" 53 | titleSecondary="Completed activities" 54 | /> 55 |
    56 |
    57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/views/Assistant.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { Heading, HEADING } from 'components'; 5 | import { useAppState } from 'modules/app'; 6 | import { useAssistant, ChatMessage, ChatControls } from 'modules/assistant'; 7 | import { getUserData } from 'modules/user'; 8 | import { ReactComponent as SpeechBubbleIcon } from 'assets/icons/speech_bubble.svg'; 9 | 10 | export const Assistant = () => { 11 | const chatBottomRef = React.useRef(null); 12 | const [state, api] = useAssistant(); 13 | const { userData } = useSelector(getUserData); 14 | const [, { setAppTheme }] = useAppState(); 15 | const { messages } = state; 16 | 17 | const handleMessagesUpdate = React.useCallback(() => { 18 | if (!chatBottomRef || !chatBottomRef.current) return; 19 | chatBottomRef.current.scrollIntoView({ behavior: 'smooth' }); 20 | }, []); 21 | 22 | useEffect(handleMessagesUpdate, [messages]); 23 | 24 | useEffect(() => { 25 | setAppTheme({ 26 | color: '#6A62FF', 27 | shapeClass: 'app__deco--default', 28 | showNav: true, 29 | }); 30 | }, [setAppTheme]); 31 | 32 | return ( 33 |
    34 | 39 |
    40 | {state.isLoading && messages.length === 0 && ( 41 |
    42 |
    43 |
    44 |
    45 | 46 |
    47 |
    48 | 49 | Couchy is waking up... 50 | 51 |
    52 |
    53 | )} 54 | {messages?.map((item) => { 55 | const [firstAnswer, secondAnswer] = item.body.split('|'); 56 | if (secondAnswer) { 57 | return ( 58 | 64 | ); 65 | } 66 | 67 | return ( 68 | 72 | {item.body} 73 | 74 | ); 75 | })} 76 |
    77 |
    78 |
    79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/views/Contacts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | import { 5 | useContactsServices, 6 | UserContact, 7 | AddContactModal, 8 | ContactModal, 9 | ContactGroup, 10 | } from 'modules/contacts'; 11 | import { Button, BUTTON, Heading, HEADING } from 'components'; 12 | import { useModalControls } from 'modules/modal'; 13 | import { ReactComponent as PlusIcon } from 'assets/icons/plus.svg'; 14 | import { ReactComponent as PlusLargeIcon } from 'assets/icons/plus_large.svg'; 15 | import { getGroupedContacts } from 'util/mapping'; 16 | import { useAppState } from 'modules/app'; 17 | 18 | export type GroupedContacts = { 19 | [k: string]: { date: Date; contacts: UserContact[] }; 20 | }; 21 | 22 | export const Contacts = () => { 23 | const [isModalOpen, { toggleModalState }] = useModalControls(); 24 | const [currentContact, setCurrentContact] = React.useState(); 25 | const [ 26 | { userContacts }, 27 | { getLastUserContacts, removeContact }, 28 | ] = useContactsServices(); 29 | const [, { setAppTheme }] = useAppState(); 30 | 31 | React.useEffect(() => { 32 | setAppTheme({ 33 | color: '#F7CE53', 34 | shapeClass: 'app__deco--default', 35 | showNav: true, 36 | }); 37 | }, [setAppTheme]); 38 | 39 | React.useEffect(() => { 40 | getLastUserContacts(); 41 | }, [getLastUserContacts]); 42 | 43 | const groupedContacts = React.useMemo( 44 | () => userContacts && getGroupedContacts(userContacts), 45 | [userContacts], 46 | ); 47 | 48 | if (!groupedContacts) return null; 49 | 50 | return ( 51 |
    52 | 62 |
    63 |
    64 | Keep track of people you see and don't forget to check up on them 65 |
    66 | {groupedContacts && isEmpty(groupedContacts) ? ( 67 | 74 | ) : ( 75 | 80 | )} 81 | 85 | 89 |
    90 |
    91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /src/views/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Redirect, Link } from 'react-router-dom'; 3 | 4 | import { LogOut, useUserServices } from 'modules/user'; 5 | import { useSettingsServices } from 'modules/settings'; 6 | import { useScoreListener, ScoreTracker } from 'modules/score'; 7 | import { ActivitySummary } from 'modules/activities'; 8 | import { ContactSummary } from 'modules/contacts'; 9 | import { useAppState } from 'modules/app'; 10 | import { Heading, HEADING } from 'components'; 11 | 12 | const Dashboard: React.FC = () => { 13 | const [{ userData }] = useUserServices(); 14 | const [{ userSettings }] = useSettingsServices(); 15 | const [, { setAppTheme }] = useAppState(); 16 | 17 | useScoreListener(userData); 18 | 19 | React.useEffect(() => { 20 | if (userData) { 21 | setAppTheme({ 22 | color: '#F7CE53', 23 | shapeClass: 'app__deco--default', 24 | showNav: true, 25 | }); 26 | } 27 | }, [setAppTheme, userData]); 28 | 29 | if (!userData) return ; 30 | 31 | if (userSettings && !userSettings.surveyCompleted) 32 | return ; 33 | 34 | return ( 35 |
    36 | 46 | 47 | 48 | 49 | 50 | 51 |
    52 | 53 |
    54 |
    55 | ); 56 | }; 57 | 58 | export { Dashboard }; 59 | -------------------------------------------------------------------------------- /src/views/GeneralError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Heading, HEADING } from 'components'; 4 | 5 | export const GeneralError = () => { 6 | return ( 7 |
    8 | 13 |
    14 | Woops, you entered an unsafe area. Keep your social distance and put 15 | your masks on! 16 |
    17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/views/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { LoginWithGoogle, LoginWithEmail } from 'modules/user'; 5 | import { useAppState } from 'modules/app'; 6 | import { Heading, HEADING } from 'components'; 7 | 8 | const Login: React.FC = () => { 9 | const [, { setAppTheme }] = useAppState(); 10 | 11 | React.useEffect(() => { 12 | setAppTheme({ 13 | color: '#6A62FF', 14 | shapeClass: 'app__deco--default', 15 | showNav: false, 16 | }); 17 | }, [setAppTheme]); 18 | 19 | return ( 20 |
    21 |
    22 |
    23 | 24 | Hi there, welcome back 25 | 26 |
    27 |

    Wow, you're a regular! We like that.

    28 |
    29 |
    30 |
    31 | 32 |
    33 |

    or use

    34 |
    35 | Log in with Google 36 |
    37 |

    38 | Don't have an account? 39 | Sign up 40 |

    41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export { Login }; 47 | -------------------------------------------------------------------------------- /src/views/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { Heading, HEADING, BUTTON } from 'components'; 5 | import { useAppState } from 'modules/app'; 6 | 7 | export const NotFound = () => { 8 | const [, { setAppTheme }] = useAppState(); 9 | 10 | React.useEffect(() => { 11 | setAppTheme({ 12 | color: '#F7CE53', 13 | shapeClass: 'app__deco--default', 14 | showNav: false, 15 | }); 16 | }, [setAppTheme]); 17 | 18 | return ( 19 |
    20 | 25 |
    26 |
    27 | 28 | It seems you got lost, but at least you're safe at home. 29 | 30 |
    31 |
    32 | 33 | Go back 34 | 35 |
    36 |
    37 |
    38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/views/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { useUserServices, UpdateProfile } from 'modules/user'; 5 | import { useAppState } from 'modules/app'; 6 | import { ScoreTracker } from 'modules/score'; 7 | import { UpdateSettings } from 'modules/settings'; 8 | import { ReactComponent as IconBack } from 'assets/icons/chevron_left.svg'; 9 | 10 | export const Profile = () => { 11 | const [{ userData }] = useUserServices(); 12 | const [, { setAppTheme }] = useAppState(); 13 | 14 | React.useEffect(() => { 15 | if (userData) { 16 | setAppTheme({ 17 | color: '#F7CE53', 18 | shapeClass: 'app__deco--default', 19 | showNav: false, 20 | }); 21 | } 22 | }, [setAppTheme, userData]); 23 | return ( 24 |
    25 | 31 |
    32 | 33 | 34 |
    35 |
    36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/views/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { SignUpEmail, LoginWithGoogle } from 'modules/user'; 5 | import { useAppState } from 'modules/app'; 6 | import { Heading, HEADING } from 'components'; 7 | 8 | const SignUp: React.FC = () => { 9 | const [, { setAppTheme }] = useAppState(); 10 | 11 | React.useEffect(() => { 12 | setAppTheme({ 13 | color: '#6A62FF', 14 | shapeClass: 'app__deco--default', 15 | showNav: false, 16 | }); 17 | }, [setAppTheme]); 18 | 19 | return ( 20 |
    21 |
    22 |
    23 | 24 | Sign up right now 25 | 26 |
    27 |

    28 | It's not like you've got anything better to do at the moment 29 |

    30 |
    31 |
    32 | 33 |

    or

    34 |

    35 | Use Google to signup 36 |

    37 |

    38 | Have an account? 39 | Log in 40 |

    41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export { SignUp }; 47 | -------------------------------------------------------------------------------- /src/views/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SplashSettings } from 'modules/settings'; 4 | import { useUserServices } from 'modules/user'; 5 | import { useAppState } from 'modules/app'; 6 | 7 | const Welcome: React.FC = () => { 8 | const [{ userData }] = useUserServices(); 9 | const [, { setAppTheme }] = useAppState(); 10 | 11 | React.useEffect(() => { 12 | if (userData) { 13 | setAppTheme({ 14 | color: '#6A62FF', 15 | shapeClass: 'app__deco--default', 16 | showNav: false, 17 | }); 18 | } 19 | }, [setAppTheme, userData]); 20 | 21 | return ( 22 |
    23 | 24 |
    25 | ); 26 | }; 27 | 28 | export { Welcome }; 29 | -------------------------------------------------------------------------------- /src/views/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Assistant'; 2 | export * from './Dashboard'; 3 | export * from './GeneralError'; 4 | export * from './Login'; 5 | export * from './SignUp'; 6 | export * from './Welcome'; 7 | export * from './Contacts'; 8 | export * from './Activities'; 9 | export * from './NotFound'; 10 | export * from './Profile'; 11 | -------------------------------------------------------------------------------- /src/workers/index.ts: -------------------------------------------------------------------------------- 1 | import * as serviceWorker from './serviceWorker'; 2 | export { serviceWorker }; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react" 18 | }, 19 | "include": ["src"], 20 | "exclude": ["server"] 21 | } 22 | --------------------------------------------------------------------------------