├── .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 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/checkbox.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/chevron_left.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/circle_ring.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/circle_star.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/close.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/google.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/person.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/plus_large.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/speech_bubble.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/icons/squares.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/text_bubble.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/icons/trophy.svg:
--------------------------------------------------------------------------------
1 |
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 ;
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 | }
42 | onClick={toggleModalState}
43 | className={BUTTON.SQUARE.LARGE.PRIMARY}
44 | >
45 | {userActivities && isEmpty(userActivities)
46 | ? 'Add tasks'
47 | : 'Add more tasks'}
48 |
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 |
:
}
54 | className={
55 | isLight ? BUTTON.SQUARE.LARGE.PRIMARY : BUTTON.SQUARE.LARGE.CTA
56 | }
57 | >
58 | {label}
59 |
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 | }
33 | className={BUTTON.SQUARE.LARGE.CTA}
34 | >
35 | Add tasks
36 |
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 |
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 |
26 |
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 |
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 | }
29 | onClick={() => removeContact(contact.id)}
30 | />{' '}
31 | {contact.name}
32 |
33 | {contact.phoneNumber && (
34 | }
37 | onClick={() => setCurrentContact(contact)}
38 | />
39 | )}
40 |
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 | }
27 | className={BUTTON.SQUARE.LARGE.CTA}
28 | >
29 | Add contacts
30 |
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 |
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 |
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 | }
20 | onClick={handleLoginGoogle}
21 | >
22 | {children}
23 |
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 |
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 |
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 |
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 | }
70 | className={BUTTON.SQUARE.LARGE.CTA}
71 | >
72 | Add contacts
73 |
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 |
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 |
--------------------------------------------------------------------------------