├── .env ├── .github ├── dependabot.yml ├── gifs │ └── showcase.gif └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── apple-touch-icon.png ├── favicon-192x192.png ├── favicon-512x512.png ├── favicon.ico ├── images │ ├── logged_in │ │ ├── image1.jpg │ │ ├── image10.jpg │ │ ├── image2.jpg │ │ ├── image3.jpg │ │ ├── image4.jpg │ │ ├── image5.jpg │ │ ├── image6.jpg │ │ ├── image7.jpg │ │ ├── image8.jpg │ │ ├── image9.jpg │ │ └── profilePicture.jpg │ └── logged_out │ │ ├── blogPost1.jpg │ │ ├── blogPost2.jpg │ │ ├── blogPost3.jpg │ │ ├── blogPost4.jpg │ │ ├── blogPost5.jpg │ │ ├── blogPost6.jpg │ │ └── headerImage.jpg ├── index.html ├── manifest.json └── robots.txt └── src ├── App.js ├── App.test.js ├── GlobalStyles.js ├── index.js ├── logged_in ├── components │ ├── Main.js │ ├── Routing.js │ ├── dashboard │ │ ├── AccountInformationArea.js │ │ ├── Dashboard.js │ │ ├── Settings1.js │ │ ├── Settings2.js │ │ ├── SettingsArea.js │ │ ├── StatisticsArea.js │ │ └── UserDataArea.js │ ├── navigation │ │ ├── Balance.js │ │ ├── MessageListItem.js │ │ ├── MessagePopperButton.js │ │ ├── NavBar.js │ │ └── SideDrawer.js │ ├── posts │ │ ├── AddPost.js │ │ ├── AddPostOptions.js │ │ ├── PostContent.js │ │ └── Posts.js │ └── subscription │ │ ├── AddBalanceDialog.js │ │ ├── LazyLoadAddBalanceDialog.js │ │ ├── Subscription.js │ │ ├── SubscriptionInfo.js │ │ ├── SubscriptionTable.js │ │ └── stripe │ │ ├── StripeCardForm.js │ │ ├── StripeIBANForm.js │ │ └── StripeTextField.js └── dummy_data │ └── persons.js ├── logged_out ├── components │ ├── Main.js │ ├── Routing.js │ ├── blog │ │ ├── Blog.js │ │ ├── BlogCard.js │ │ └── BlogPost.js │ ├── cookies │ │ ├── CookieConsent.js │ │ ├── CookieRulesDialog.js │ │ └── fetchIpData.js │ ├── footer │ │ └── Footer.js │ ├── home │ │ ├── FeatureCard.js │ │ ├── FeatureSection.js │ │ ├── HeadSection.js │ │ ├── Home.js │ │ ├── PriceCard.js │ │ ├── PricingSection.js │ │ └── calculateSpacing.js │ ├── navigation │ │ └── NavBar.js │ └── register_login │ │ ├── ChangePasswordDialog.js │ │ ├── DialogSelector.js │ │ ├── LoginDialog.js │ │ ├── RegisterDialog.js │ │ └── TermsOfServiceDialog.js └── dummy_data │ └── blogPosts.js ├── service-worker.js ├── serviceWorkerRegistration.js ├── setupTests.js ├── shared ├── components │ ├── ActionPaper.js │ ├── Bordered.js │ ├── ButtonCircularProgress.js │ ├── CardChart.js │ ├── ColoredButton.js │ ├── ColorfulChip.js │ ├── ConfirmationDialog.js │ ├── ConsecutiveSnackbarMessages.js │ ├── DateTimePicker.js │ ├── DialogTitleWithCloseIcon.js │ ├── Dropzone.js │ ├── EmojiTextArea.js │ ├── EnhancedTableHead.js │ ├── FormDialog.js │ ├── HelpIcon.js │ ├── HighlightedInformation.js │ ├── ImageCropper.js │ ├── ImageCropperDialog.js │ ├── ModalBackdrop.js │ ├── NavigationDrawer.js │ ├── Pace.js │ ├── PropsRoute.js │ ├── SelfAligningImage.js │ ├── ShareButton.js │ ├── ShareButtons.js │ ├── VertOptions.js │ ├── VisibilityPasswordTextField.js │ ├── WaveBorder.js │ └── ZoomImage.js └── functions │ ├── countWithEmojis.js │ ├── currencyPrettyPrint.js │ ├── getSorting.js │ ├── shadeColor.js │ ├── smoothScrollTop.js │ ├── stableSort.js │ ├── toArray.js │ ├── unixToDateString.js │ ├── useLocationBlocker.js │ └── useWidth.js └── theme.js /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=src/ 2 | 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - dunky11 11 | ignore: 12 | - dependency-name: "@date-io/date-fns" 13 | versions: 14 | - ">= 2.3.a, < 2.4" 15 | - dependency-name: "@date-io/date-fns" 16 | versions: 17 | - ">= 2.4.a, < 2.5" 18 | - dependency-name: "@date-io/date-fns" 19 | versions: 20 | - ">= 2.a, < 3" 21 | - dependency-name: workbox-strategies 22 | versions: 23 | - 6.1.5 24 | - dependency-name: "@testing-library/react" 25 | versions: 26 | - 11.2.6 27 | - dependency-name: "@testing-library/user-event" 28 | versions: 29 | - 12.6.3 30 | - 12.7.2 31 | - 13.0.10 32 | - 13.0.13 33 | - 13.0.6 34 | - 13.0.7 35 | - dependency-name: workbox-google-analytics 36 | versions: 37 | - 6.1.1 38 | -------------------------------------------------------------------------------- /.github/gifs/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/.github/gifs/showcase.gif -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: ./node_modules/eslint/bin/eslint.js src --max-warnings=0 25 | - run: npm run build 26 | - run: npm run test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .idea 9 | .vscode 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # build directory 108 | build/ 109 | 110 | #lockfiles 111 | yarn.lock 112 | 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim von Känel 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 | # React SaaS Template 2 | Remains of a SaaS business I once tried to build. Now transformed into a template for building an SaaS/admin application using React + Material-UI. 3 | 4 | [**Check out the demo**](https://reactsaastemplate.com) 5 | 6 | ![Node.js CI](https://github.com/dunky11/react-saas-template/workflows/Node.js%20CI/badge.svg) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 8 | 9 | [](https://reactsaastemplate.com "Go to demo website") 10 | 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | 16 | #### Node.js 12+ (versions below could work, but are not tested) 17 | 18 | * Linux: 19 | 20 | ``` 21 | sudo apt install nodejs npm 22 | ``` 23 | 24 | * Windows or macOS: 25 | 26 | https://nodejs.org/en/ 27 | 28 | ### Installing 29 | 30 | 1. Clone the repository 31 | 32 | ``` 33 | git clone https://github.com/dunky11/react-saas-template 34 | ``` 35 | 2. Install dependencies, this can take a minute 36 | 37 | ``` 38 | cd react-saas-template 39 | npm install 40 | ``` 41 | 3. Start the local server 42 | 43 | ``` 44 | npm start 45 | ``` 46 | 47 | Your browser should now open and show the app. Otherwise open http://localhost:3000/ in your browser. Editing files will automatically refresh the page. 48 | 49 | ### What to do next? 50 | 51 | If you are new to React, you should watch a [basic React tutorial](https://www.youtube.com/results?search_query=react+tutorial) first. 52 | 53 | If you know React, then most of the information you need is in the [Material-UI documentation](https://material-ui.com/getting-started/usage/). 54 | 55 | You can go into [src/theme.js](/src/theme.js) and change the primary and secondary color codes at the top of the script to the values you like and some magic will happen. 56 | 57 | ## Deployment 58 | 59 | If you are satisfied with the state of your website you can run: 60 | 61 | ``` 62 | npm run build 63 | ``` 64 | 65 | It will create a folder named build with your compiled project inside. After that copy its content into your webroot and you are ready to go. 66 | 67 | ## Built With 68 | 69 | * [Create-React-App](https://github.com/facebook/create-react-app) - Used to bootstrap the development 70 | * [Material-UI](https://github.com/mui-org/material-ui) - Material Design components 71 | * [React-Router](https://github.com/ReactTraining/react-router) - Routing of the app 72 | * [Pace](https://github.com/HubSpot/pace) - Loading bar at the top 73 | * [Emoji-Mart](https://github.com/missive/emoji-mart) - Picker for the emojis 74 | * [React-Dropzone](https://github.com/react-dropzone/react-dropzone) - File drop component for uploads 75 | * [Recharts](https://github.com/recharts/recharts) - Charting library I used for the statistics 76 | * [Aos](https://github.com/michalsnik/aos) - Animations based on viewport 77 | * [React-Cropper](https://github.com/roadmanfong/react-cropper) - Cropper for the image uploads 78 | * [React-Stripe-js](https://github.com/stripe/react-stripe-js) - Stripes payment elements 79 | 80 | ## Contribute 81 | Show your support by ⭐ the project. Pull requests are always welcome. 82 | 83 | ## License 84 | 85 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/dunky11/react-saas-template/blob/master/LICENSE) file for details. 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-saas-template", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": "", 6 | "description": "Template for building an SaaS/admin application using React + Material-UI", 7 | "dependencies": { 8 | "@date-io/date-fns": "^1.3.13", 9 | "@emotion/react": "^11.8.1", 10 | "@emotion/styled": "^11.8.1", 11 | "@mui/icons-material": "^5.4.4", 12 | "@mui/lab": "^5.0.0-alpha.71", 13 | "@mui/material": "^5.4.4", 14 | "@mui/styles": "^5.4.4", 15 | "@stripe/react-stripe-js": "^1.4.1", 16 | "@stripe/stripe-js": "^1.16.0", 17 | "aos": "^2.3.4", 18 | "classnames": "^2.3.1", 19 | "emoji-mart": "^3.0.1", 20 | "js-cookie": "^3.0.1", 21 | "prop-types": "^15.7.2", 22 | "react": "^17.0.2", 23 | "react-cropper": "^2.1.8", 24 | "react-dom": "^17.0.2", 25 | "react-dropzone": "^11.3.4", 26 | "react-router": "^5.2.0", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "^2.1.3", 29 | "recharts": "^2.1.2", 30 | "workbox-background-sync": "^6.2.4", 31 | "workbox-broadcast-update": "^6.2.2", 32 | "workbox-cacheable-response": "^6.2.4", 33 | "workbox-core": "^6.1.5", 34 | "workbox-expiration": "^6.1.5", 35 | "workbox-google-analytics": "^6.2.4", 36 | "workbox-navigation-preload": "^6.2.2", 37 | "workbox-precaching": "^6.2.4", 38 | "workbox-range-requests": "^6.1.5", 39 | "workbox-routing": "^6.2.4", 40 | "workbox-strategies": "^6.1.5", 41 | "workbox-streams": "^6.1.5" 42 | }, 43 | "devDependencies": { 44 | "@testing-library/jest-dom": "^5.14.1", 45 | "@testing-library/react": "^12.0.0", 46 | "@testing-library/user-event": "^13.2.1" 47 | }, 48 | "scripts": { 49 | "start": "react-scripts start", 50 | "build": "react-scripts build", 51 | "test": "react-scripts test", 52 | "eject": "react-scripts eject" 53 | }, 54 | "eslintConfig": { 55 | "extends": "react-app" 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/dunky11/react-saas-template.git" 72 | }, 73 | "author": "dunky11", 74 | "license": "MIT", 75 | "bugs": { 76 | "url": "https://github.com/dunky11/react-saas-template/issues" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/favicon-512x512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logged_in/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image1.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image10.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image2.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image3.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image4.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image5.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image6.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image7.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image8.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/image9.jpg -------------------------------------------------------------------------------- /public/images/logged_in/profilePicture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_in/profilePicture.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost1.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost2.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost3.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost4.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost5.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/blogPost6.jpg -------------------------------------------------------------------------------- /public/images/logged_out/headerImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunky11/react-saas-template/3fff0ba1ebd1a05b75a6484eea92fdad93b06e6d/public/images/logged_out/headerImage.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 18 | 19 | 20 | 25 | 34 | 35 | 38 | 42 | 43 | WaVer - Free React template for building an SaaS or admin application 44 | 45 | 46 | 47 | 48 |
49 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WaVer", 3 | "name": "WaVer react template", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "favicon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicon-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": "./index.html", 22 | "display": "standalone", 23 | "theme_color": "#b3294e", 24 | "background_color": "#f5f5f5" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, Suspense, lazy } from "react"; 2 | import { ThemeProvider, StyledEngineProvider, CssBaseline } from "@mui/material"; 3 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 4 | import theme from "./theme"; 5 | import GlobalStyles from "./GlobalStyles"; 6 | import Pace from "./shared/components/Pace"; 7 | 8 | const LoggedInComponent = lazy(() => import("./logged_in/components/Main")); 9 | 10 | const LoggedOutComponent = lazy(() => import("./logged_out/components/Main")); 11 | 12 | function App() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | }> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | test("Trying to render ", () => { 6 | render(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/GlobalStyles.js: -------------------------------------------------------------------------------- 1 | import withStyles from '@mui/styles/withStyles'; 2 | 3 | const styles = theme => ({ 4 | "@global": { 5 | /** 6 | * Disable the focus outline, which is default on some browsers like 7 | * chrome when focusing elements 8 | */ 9 | "*:focus": { 10 | outline: 0 11 | }, 12 | ".text-white": { 13 | color: theme.palette.common.white 14 | }, 15 | ".listItemLeftPadding": { 16 | paddingTop: `${theme.spacing(1.75)} !important`, 17 | paddingBottom: `${theme.spacing(1.75)} !important`, 18 | paddingLeft: `${theme.spacing(4)} !important`, 19 | [theme.breakpoints.down('md')]: { 20 | paddingLeft: `${theme.spacing(4)} !important` 21 | }, 22 | "@media (max-width: 420px)": { 23 | paddingLeft: `${theme.spacing(1)} !important` 24 | } 25 | }, 26 | ".container": { 27 | width: "100%", 28 | paddingRight: theme.spacing(4), 29 | paddingLeft: theme.spacing(4), 30 | marginRight: "auto", 31 | marginLeft: "auto", 32 | [theme.breakpoints.up("sm")]: { 33 | maxWidth: 540 34 | }, 35 | [theme.breakpoints.up("md")]: { 36 | maxWidth: 720 37 | }, 38 | [theme.breakpoints.up("lg")]: { 39 | maxWidth: 1170 40 | } 41 | }, 42 | ".row": { 43 | display: "flex", 44 | flexWrap: "wrap", 45 | marginRight: -theme.spacing(2), 46 | marginLeft: -theme.spacing(2) 47 | }, 48 | ".container-fluid": { 49 | width: "100%", 50 | paddingRight: theme.spacing(2), 51 | paddingLeft: theme.spacing(2), 52 | marginRight: "auto", 53 | marginLeft: "auto", 54 | maxWidth: 1370 55 | }, 56 | ".lg-mg-top": { 57 | marginTop: `${theme.spacing(20)} !important`, 58 | [theme.breakpoints.down('lg')]: { 59 | marginTop: `${theme.spacing(18)} !important` 60 | }, 61 | [theme.breakpoints.down('md')]: { 62 | marginTop: `${theme.spacing(16)} !important` 63 | }, 64 | [theme.breakpoints.down('sm')]: { 65 | marginTop: `${theme.spacing(14)} !important` 66 | } 67 | }, 68 | ".lg-mg-bottom": { 69 | marginBottom: `${theme.spacing(20)} !important`, 70 | [theme.breakpoints.down('lg')]: { 71 | marginBottom: `${theme.spacing(18)} !important` 72 | }, 73 | [theme.breakpoints.down('md')]: { 74 | marginBottom: `${theme.spacing(16)} !important` 75 | }, 76 | [theme.breakpoints.down('sm')]: { 77 | marginBottom: `${theme.spacing(14)} !important` 78 | } 79 | }, 80 | ".lg-p-top": { 81 | paddingTop: `${theme.spacing(20)} !important`, 82 | [theme.breakpoints.down('lg')]: { 83 | paddingTop: `${theme.spacing(18)} !important` 84 | }, 85 | [theme.breakpoints.down('md')]: { 86 | paddingTop: `${theme.spacing(16)} !important` 87 | }, 88 | [theme.breakpoints.down('sm')]: { 89 | paddingTop: `${theme.spacing(14)} !important` 90 | } 91 | } 92 | } 93 | }); 94 | 95 | function globalStyles() { 96 | return null; 97 | } 98 | 99 | export default withStyles(styles, { withTheme: true })(globalStyles); 100 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById("root") 9 | ); 10 | 11 | serviceWorkerRegistration.register(); 12 | -------------------------------------------------------------------------------- /src/logged_in/components/Routing.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Switch } from "react-router-dom"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import Dashboard from "./dashboard/Dashboard"; 6 | import Posts from "./posts/Posts"; 7 | import Subscription from "./subscription/Subscription"; 8 | import PropsRoute from "../../shared/components/PropsRoute"; 9 | import useLocationBlocker from "../../shared/functions/useLocationBlocker"; 10 | 11 | const styles = (theme) => ({ 12 | wrapper: { 13 | margin: theme.spacing(1), 14 | width: "auto", 15 | [theme.breakpoints.up("xs")]: { 16 | width: "95%", 17 | marginLeft: "auto", 18 | marginRight: "auto", 19 | marginTop: theme.spacing(4), 20 | marginBottom: theme.spacing(4), 21 | }, 22 | [theme.breakpoints.up("sm")]: { 23 | marginTop: theme.spacing(6), 24 | marginBottom: theme.spacing(6), 25 | width: "90%", 26 | marginLeft: "auto", 27 | marginRight: "auto", 28 | }, 29 | [theme.breakpoints.up("md")]: { 30 | marginTop: theme.spacing(6), 31 | marginBottom: theme.spacing(6), 32 | width: "82.5%", 33 | marginLeft: "auto", 34 | marginRight: "auto", 35 | }, 36 | [theme.breakpoints.up("lg")]: { 37 | marginTop: theme.spacing(6), 38 | marginBottom: theme.spacing(6), 39 | width: "70%", 40 | marginLeft: "auto", 41 | marginRight: "auto", 42 | }, 43 | }, 44 | }); 45 | 46 | function Routing(props) { 47 | const { 48 | classes, 49 | EmojiTextArea, 50 | ImageCropper, 51 | Dropzone, 52 | DateTimePicker, 53 | pushMessageToSnackbar, 54 | posts, 55 | transactions, 56 | toggleAccountActivation, 57 | CardChart, 58 | statistics, 59 | targets, 60 | setTargets, 61 | setPosts, 62 | isAccountActivated, 63 | selectDashboard, 64 | selectPosts, 65 | selectSubscription, 66 | openAddBalanceDialog, 67 | } = props; 68 | useLocationBlocker(); 69 | return ( 70 |
71 | 72 | 84 | 92 | 104 | 105 |
106 | ); 107 | } 108 | 109 | Routing.propTypes = { 110 | classes: PropTypes.object.isRequired, 111 | EmojiTextArea: PropTypes.elementType, 112 | ImageCropper: PropTypes.elementType, 113 | Dropzone: PropTypes.elementType, 114 | DateTimePicker: PropTypes.elementType, 115 | pushMessageToSnackbar: PropTypes.func, 116 | setTargets: PropTypes.func.isRequired, 117 | setPosts: PropTypes.func.isRequired, 118 | posts: PropTypes.arrayOf(PropTypes.object).isRequired, 119 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired, 120 | toggleAccountActivation: PropTypes.func, 121 | CardChart: PropTypes.elementType, 122 | statistics: PropTypes.object.isRequired, 123 | targets: PropTypes.arrayOf(PropTypes.object).isRequired, 124 | isAccountActivated: PropTypes.bool.isRequired, 125 | selectDashboard: PropTypes.func.isRequired, 126 | selectPosts: PropTypes.func.isRequired, 127 | selectSubscription: PropTypes.func.isRequired, 128 | openAddBalanceDialog: PropTypes.func.isRequired, 129 | }; 130 | 131 | export default withStyles(styles, { withTheme: true })(memo(Routing)); 132 | -------------------------------------------------------------------------------- /src/logged_in/components/dashboard/AccountInformationArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { 5 | Paper, 6 | Toolbar, 7 | ListItemText, 8 | ListItemSecondaryAction, 9 | ListItemIcon, 10 | Switch, 11 | Box, 12 | } from "@mui/material"; 13 | import withStyles from '@mui/styles/withStyles'; 14 | import LoopIcon from "@mui/icons-material/Loop"; 15 | 16 | const styles = theme => ({ 17 | paper: { 18 | borderBottomLeftRadius: 0, 19 | borderBottomRightRadius: 0 20 | }, 21 | toolbar: { justifyContent: "space-between" }, 22 | scaleMinus: { 23 | transform: "scaleX(-1)" 24 | }, 25 | "@keyframes spin": { 26 | from: { transform: "rotate(359deg)" }, 27 | to: { transform: "rotate(0deg)" } 28 | }, 29 | spin: { animation: "$spin 2s infinite linear" }, 30 | listItemSecondaryAction: { paddingRight: theme.spacing(1) } 31 | }); 32 | 33 | function AccountInformationArea(props) { 34 | const { classes, toggleAccountActivation, isAccountActivated } = props; 35 | return ( 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 66 | 67 | 68 | 69 | ); 70 | } 71 | 72 | AccountInformationArea.propTypes = { 73 | classes: PropTypes.object.isRequired, 74 | theme: PropTypes.object.isRequired, 75 | toggleAccountActivation: PropTypes.func.isRequired, 76 | isAccountActivated: PropTypes.bool.isRequired 77 | }; 78 | 79 | export default withStyles(styles, { withTheme: true })(AccountInformationArea); 80 | -------------------------------------------------------------------------------- /src/logged_in/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Typography, Box } from "@mui/material"; 4 | import SettingsArea from "./SettingsArea"; 5 | import UserDataArea from "./UserDataArea"; 6 | import AccountInformationArea from "./AccountInformationArea"; 7 | import StatisticsArea from "./StatisticsArea"; 8 | 9 | function Dashboard(props) { 10 | const { 11 | selectDashboard, 12 | CardChart, 13 | statistics, 14 | toggleAccountActivation, 15 | pushMessageToSnackbar, 16 | targets, 17 | setTargets, 18 | isAccountActivated, 19 | } = props; 20 | 21 | useEffect(selectDashboard, [selectDashboard]); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | Your Account 29 | 30 | 31 | 35 | 36 | 37 | Settings 38 | 39 | 40 | 41 | 46 | 47 | ); 48 | } 49 | 50 | Dashboard.propTypes = { 51 | CardChart: PropTypes.elementType, 52 | statistics: PropTypes.object.isRequired, 53 | toggleAccountActivation: PropTypes.func, 54 | pushMessageToSnackbar: PropTypes.func, 55 | targets: PropTypes.arrayOf(PropTypes.object).isRequired, 56 | setTargets: PropTypes.func.isRequired, 57 | isAccountActivated: PropTypes.bool.isRequired, 58 | selectDashboard: PropTypes.func.isRequired, 59 | }; 60 | 61 | export default Dashboard; 62 | -------------------------------------------------------------------------------- /src/logged_in/components/dashboard/SettingsArea.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Settings1 from "./Settings1"; 4 | import Settings2 from "./Settings2"; 5 | 6 | function SettingsArea(props) { 7 | const { pushMessageToSnackbar } = props; 8 | return ( 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | SettingsArea.propTypes = { 17 | pushMessageToSnackbar: PropTypes.func 18 | }; 19 | 20 | export default SettingsArea; 21 | -------------------------------------------------------------------------------- /src/logged_in/components/dashboard/StatisticsArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Grid } from "@mui/material"; 4 | 5 | import withTheme from '@mui/styles/withTheme'; 6 | 7 | function StatisticsArea(props) { 8 | const { theme, CardChart, data } = props; 9 | return ( 10 | CardChart && 11 | data.profit.length >= 2 && 12 | data.views.length >= 2 && ( 13 | 14 | 15 | 21 | 22 | 23 | 29 | 30 | 31 | ) 32 | ); 33 | } 34 | 35 | StatisticsArea.propTypes = { 36 | theme: PropTypes.object.isRequired, 37 | data: PropTypes.object.isRequired, 38 | CardChart: PropTypes.elementType 39 | }; 40 | 41 | export default withTheme(StatisticsArea); 42 | -------------------------------------------------------------------------------- /src/logged_in/components/navigation/Balance.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { OutlinedInput } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import currencyPrettyPrint from "../../../shared/functions/currencyPrettyPrint"; 6 | 7 | const styles = { 8 | input: { padding: "0px 9px", cursor: "pointer" }, 9 | outlinedInput: { 10 | width: 90, 11 | height: 40, 12 | cursor: "pointer" 13 | }, 14 | wrapper: { 15 | display: "flex", 16 | alignItems: "center" 17 | } 18 | }; 19 | 20 | function Balance(props) { 21 | const { balance, classes, openAddBalanceDialog } = props; 22 | return ( 23 |
24 | 32 |
33 | ); 34 | } 35 | 36 | Balance.propTypes = { 37 | balance: PropTypes.number.isRequired, 38 | classes: PropTypes.object.isRequired, 39 | openAddBalanceDialog: PropTypes.func.isRequired 40 | }; 41 | 42 | export default withStyles(styles)(Balance); 43 | -------------------------------------------------------------------------------- /src/logged_in/components/navigation/MessageListItem.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | ListItem, 5 | ListItemAvatar, 6 | ListItemText, 7 | Avatar, 8 | } from "@mui/material"; 9 | import ErrorIcon from "@mui/icons-material/Error"; 10 | import formatDistance from "date-fns/formatDistance"; 11 | 12 | function MessageListItem(props) { 13 | const { message, divider } = props; 14 | const [hasErrorOccurred, setHasErrorOccurred] = useState(false); 15 | 16 | const handleError = useCallback(() => { 17 | setHasErrorOccurred(true); 18 | }, [setHasErrorOccurred]); 19 | 20 | return ( 21 | 22 | 23 | {hasErrorOccurred ? ( 24 | 25 | ) : ( 26 | 30 | )} 31 | 32 | 36 | 37 | ); 38 | } 39 | 40 | MessageListItem.propTypes = { 41 | message: PropTypes.object.isRequired, 42 | divider: PropTypes.bool, 43 | }; 44 | 45 | export default MessageListItem; 46 | -------------------------------------------------------------------------------- /src/logged_in/components/navigation/MessagePopperButton.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useRef, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Popover, 5 | IconButton, 6 | AppBar, 7 | List, 8 | Divider, 9 | ListItem, 10 | ListItemText, 11 | Typography, 12 | Box, 13 | } from "@mui/material"; 14 | import withStyles from '@mui/styles/withStyles'; 15 | import MessageIcon from "@mui/icons-material/Message"; 16 | import MessageListItem from "./MessageListItem"; 17 | 18 | const styles = (theme) => ({ 19 | tabContainer: { 20 | overflowY: "auto", 21 | maxHeight: 350, 22 | }, 23 | popoverPaper: { 24 | width: "100%", 25 | maxWidth: 350, 26 | marginLeft: theme.spacing(2), 27 | marginRight: theme.spacing(1), 28 | [theme.breakpoints.down('md')]: { 29 | maxWidth: 270, 30 | }, 31 | }, 32 | divider: { 33 | marginTop: -2, 34 | }, 35 | noShadow: { 36 | boxShadow: "none !important", 37 | }, 38 | }); 39 | 40 | function MessagePopperButton(props) { 41 | const { classes, messages } = props; 42 | const anchorEl = useRef(); 43 | const [isOpen, setIsOpen] = useState(false); 44 | 45 | const handleClick = useCallback(() => { 46 | setIsOpen(!isOpen); 47 | }, [isOpen, setIsOpen]); 48 | 49 | const handleClickAway = useCallback(() => { 50 | setIsOpen(false); 51 | }, [setIsOpen]); 52 | 53 | const id = isOpen ? "scroll-playground" : null; 54 | return ( 55 | 56 | 63 | 64 | 65 | 81 | 82 | 83 | Messages 84 | 85 | 86 | 87 | 88 | {messages.length === 0 ? ( 89 | 90 | 91 | You haven't received any messages yet. 92 | 93 | 94 | ) : ( 95 | messages.map((element, index) => ( 96 | 101 | )) 102 | )} 103 | 104 | 105 | 106 | ); 107 | } 108 | 109 | MessagePopperButton.propTypes = { 110 | classes: PropTypes.object.isRequired, 111 | messages: PropTypes.arrayOf(PropTypes.object).isRequired, 112 | }; 113 | 114 | export default withStyles(styles, { withTheme: true })(MessagePopperButton); 115 | -------------------------------------------------------------------------------- /src/logged_in/components/navigation/SideDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Drawer, IconButton, Toolbar, Divider, Typography, Box } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import CloseIcon from "@mui/icons-material/Close"; 6 | 7 | const drawerWidth = 240; 8 | 9 | const styles = { 10 | toolbar: { 11 | minWidth: drawerWidth 12 | } 13 | }; 14 | 15 | function SideDrawer(props) { 16 | const { classes, onClose, open } = props; 17 | return ( 18 | 19 | 20 | 28 | A Sidedrawer 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | SideDrawer.propTypes = { 44 | classes: PropTypes.object.isRequired, 45 | open: PropTypes.bool.isRequired, 46 | onClose: PropTypes.func.isRequired 47 | }; 48 | 49 | export default withStyles(styles)(SideDrawer); 50 | -------------------------------------------------------------------------------- /src/logged_in/components/posts/AddPost.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button, Box } from "@mui/material"; 4 | import ActionPaper from "../../../shared/components/ActionPaper"; 5 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 6 | import AddPostOptions from "./AddPostOptions"; 7 | 8 | function AddPost(props) { 9 | const { 10 | pushMessageToSnackbar, 11 | Dropzone, 12 | EmojiTextArea, 13 | DateTimePicker, 14 | ImageCropper, 15 | onClose, 16 | } = props; 17 | 18 | const [files, setFiles] = useState([]); 19 | const [uploadAt, setUploadAt] = useState(new Date()); 20 | const [loading, setLoading] = useState(false); 21 | const [cropperFile, setCropperFile] = useState(null); 22 | 23 | const acceptDrop = useCallback( 24 | (file) => { 25 | setFiles([file]); 26 | }, 27 | [setFiles] 28 | ); 29 | 30 | const onDrop = useCallback( 31 | (acceptedFiles, rejectedFiles) => { 32 | if (acceptedFiles.length + rejectedFiles.length > 1) { 33 | pushMessageToSnackbar({ 34 | isErrorMessage: true, 35 | text: "You cannot upload more than one file at once", 36 | }); 37 | } else if (acceptedFiles.length === 0) { 38 | pushMessageToSnackbar({ 39 | isErrorMessage: true, 40 | text: "The file you wanted to upload isn't an image", 41 | }); 42 | } else if (acceptedFiles.length === 1) { 43 | const file = acceptedFiles[0]; 44 | file.preview = URL.createObjectURL(file); 45 | file.key = new Date().getTime(); 46 | setCropperFile(file); 47 | } 48 | }, 49 | [pushMessageToSnackbar, setCropperFile] 50 | ); 51 | 52 | const onCropperClose = useCallback(() => { 53 | setCropperFile(null); 54 | }, [setCropperFile]); 55 | 56 | const deleteItem = useCallback(() => { 57 | setCropperFile(null); 58 | setFiles([]); 59 | }, [setCropperFile, setFiles]); 60 | 61 | const onCrop = useCallback( 62 | (dataUrl) => { 63 | const file = { ...cropperFile }; 64 | file.preview = dataUrl; 65 | acceptDrop(file); 66 | setCropperFile(null); 67 | }, 68 | [acceptDrop, cropperFile, setCropperFile] 69 | ); 70 | 71 | const handleUpload = useCallback(() => { 72 | setLoading(true); 73 | setTimeout(() => { 74 | pushMessageToSnackbar({ 75 | text: "Your post has been uploaded", 76 | }); 77 | onClose(); 78 | }, 1500); 79 | }, [setLoading, onClose, pushMessageToSnackbar]); 80 | 81 | return ( 82 | 83 | 101 | } 102 | actions={ 103 | 104 | 105 | 108 | 109 | 117 | 118 | } 119 | /> 120 | 121 | ); 122 | } 123 | 124 | AddPost.propTypes = { 125 | pushMessageToSnackbar: PropTypes.func, 126 | onClose: PropTypes.func, 127 | Dropzone: PropTypes.elementType, 128 | EmojiTextArea: PropTypes.elementType, 129 | DateTimePicker: PropTypes.elementType, 130 | ImageCropper: PropTypes.elementType, 131 | }; 132 | 133 | export default AddPost; 134 | -------------------------------------------------------------------------------- /src/logged_in/components/posts/PostContent.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Grid, TablePagination, Divider, Toolbar, Typography, Button, Paper, Box } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import DeleteIcon from "@mui/icons-material/Delete"; 6 | import SelfAligningImage from "../../../shared/components/SelfAligningImage"; 7 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 8 | import ConfirmationDialog from "../../../shared/components/ConfirmationDialog"; 9 | 10 | const styles = { 11 | dBlock: { display: "block" }, 12 | dNone: { display: "none" }, 13 | toolbar: { 14 | justifyContent: "space-between", 15 | }, 16 | }; 17 | 18 | const rowsPerPage = 25; 19 | 20 | function PostContent(props) { 21 | const { 22 | pushMessageToSnackbar, 23 | setPosts, 24 | posts, 25 | openAddPostModal, 26 | classes, 27 | } = props; 28 | const [page, setPage] = useState(0); 29 | const [isDeletePostDialogOpen, setIsDeletePostDialogOpen] = useState(false); 30 | const [isDeletePostDialogLoading, setIsDeletePostDialogLoading] = useState( 31 | false 32 | ); 33 | 34 | const closeDeletePostDialog = useCallback(() => { 35 | setIsDeletePostDialogOpen(false); 36 | setIsDeletePostDialogLoading(false); 37 | }, [setIsDeletePostDialogOpen, setIsDeletePostDialogLoading]); 38 | 39 | const deletePost = useCallback(() => { 40 | setIsDeletePostDialogLoading(true); 41 | setTimeout(() => { 42 | const _posts = [...posts]; 43 | const index = _posts.find((element) => element.id === deletePost.id); 44 | _posts.splice(index, 1); 45 | setPosts(_posts); 46 | pushMessageToSnackbar({ 47 | text: "Your post has been deleted", 48 | }); 49 | closeDeletePostDialog(); 50 | }, 1500); 51 | }, [ 52 | posts, 53 | setPosts, 54 | setIsDeletePostDialogLoading, 55 | pushMessageToSnackbar, 56 | closeDeletePostDialog, 57 | ]); 58 | 59 | const onDelete = useCallback(() => { 60 | setIsDeletePostDialogOpen(true); 61 | }, [setIsDeletePostDialogOpen]); 62 | 63 | const handleChangePage = useCallback( 64 | (__, page) => { 65 | setPage(page); 66 | }, 67 | [setPage] 68 | ); 69 | 70 | const printImageGrid = useCallback(() => { 71 | if (posts.length > 0) { 72 | return ( 73 | 74 | 75 | {posts 76 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 77 | .map((post) => ( 78 | 79 | { 87 | onDelete(post); 88 | }, 89 | icon: , 90 | }, 91 | ]} 92 | /> 93 | 94 | ))} 95 | 96 | 97 | ); 98 | } 99 | return ( 100 | 101 | 102 | No posts added yet. Click on "NEW" to create your first one. 103 | 104 | 105 | ); 106 | }, [posts, onDelete, page]); 107 | 108 | return ( 109 | 110 | 111 | Your Posts 112 | 120 | 121 | 122 | {printImageGrid()} 123 | 0 ? classes.dBlock : classes.dNone, 139 | caption: posts.length > 0 ? classes.dBlock : classes.dNone, 140 | }} 141 | labelRowsPerPage="" 142 | /> 143 | 151 | 152 | ); 153 | } 154 | 155 | PostContent.propTypes = { 156 | openAddPostModal: PropTypes.func.isRequired, 157 | classes: PropTypes.object.isRequired, 158 | posts: PropTypes.arrayOf(PropTypes.object).isRequired, 159 | setPosts: PropTypes.func.isRequired, 160 | pushMessageToSnackbar: PropTypes.func, 161 | }; 162 | 163 | export default withStyles(styles)(PostContent); 164 | -------------------------------------------------------------------------------- /src/logged_in/components/posts/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import PostContent from "./PostContent"; 4 | import AddPost from "./AddPost"; 5 | 6 | function Posts(props) { 7 | const { 8 | selectPosts, 9 | EmojiTextArea, 10 | ImageCropper, 11 | Dropzone, 12 | DateTimePicker, 13 | pushMessageToSnackbar, 14 | posts, 15 | setPosts, 16 | } = props; 17 | const [isAddPostPaperOpen, setIsAddPostPaperOpen] = useState(false); 18 | 19 | const openAddPostModal = useCallback(() => { 20 | setIsAddPostPaperOpen(true); 21 | }, [setIsAddPostPaperOpen]); 22 | 23 | const closeAddPostModal = useCallback(() => { 24 | setIsAddPostPaperOpen(false); 25 | }, [setIsAddPostPaperOpen]); 26 | 27 | useEffect(() => { 28 | selectPosts(); 29 | }, [selectPosts]); 30 | 31 | if (isAddPostPaperOpen) { 32 | return 40 | } 41 | return 47 | } 48 | 49 | Posts.propTypes = { 50 | EmojiTextArea: PropTypes.elementType, 51 | ImageCropper: PropTypes.elementType, 52 | Dropzone: PropTypes.elementType, 53 | DateTimePicker: PropTypes.elementType, 54 | posts: PropTypes.arrayOf(PropTypes.object).isRequired, 55 | setPosts: PropTypes.func.isRequired, 56 | pushMessageToSnackbar: PropTypes.func, 57 | selectPosts: PropTypes.func.isRequired, 58 | }; 59 | 60 | export default Posts; 61 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/AddBalanceDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { loadStripe } from "@stripe/stripe-js"; 4 | import { 5 | Elements, 6 | CardElement, 7 | IbanElement, 8 | useStripe, 9 | useElements 10 | } from "@stripe/react-stripe-js"; 11 | import { Grid, Button, Box } from "@mui/material"; 12 | import withTheme from '@mui/styles/withTheme'; 13 | import StripeCardForm from "./stripe/StripeCardForm"; 14 | import StripeIbanForm from "./stripe/StripeIBANForm"; 15 | import FormDialog from "../../../shared/components/FormDialog"; 16 | import ColoredButton from "../../../shared/components/ColoredButton"; 17 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 18 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 19 | 20 | const stripePromise = loadStripe("pk_test_6pRNASCoBOKtIshFeQd4XMUh"); 21 | 22 | const paymentOptions = ["Credit Card", "SEPA Direct Debit"]; 23 | 24 | const AddBalanceDialog = withTheme(function (props) { 25 | const { open, theme, onClose, onSuccess } = props; 26 | 27 | const [loading, setLoading] = useState(false); 28 | const [paymentOption, setPaymentOption] = useState("Credit Card"); 29 | const [stripeError, setStripeError] = useState(""); 30 | const [name, setName] = useState(""); 31 | const [email, setEmail] = useState(""); 32 | const [amount, setAmount] = useState(0); 33 | const [amountError, setAmountError] = useState(""); 34 | const elements = useElements(); 35 | const stripe = useStripe(); 36 | 37 | const onAmountChange = amount => { 38 | if (amount < 0) { 39 | return; 40 | } 41 | if (amountError) { 42 | setAmountError(""); 43 | } 44 | setAmount(amount); 45 | }; 46 | 47 | const getStripePaymentInfo = () => { 48 | switch (paymentOption) { 49 | case "Credit Card": { 50 | return { 51 | type: "card", 52 | card: elements.getElement(CardElement), 53 | billing_details: { name: name } 54 | }; 55 | } 56 | case "SEPA Direct Debit": { 57 | return { 58 | type: "sepa_debit", 59 | sepa_debit: elements.getElement(IbanElement), 60 | billing_details: { email: email, name: name } 61 | }; 62 | } 63 | default: 64 | throw new Error("No case selected in switch statement"); 65 | } 66 | }; 67 | 68 | const renderPaymentComponent = () => { 69 | switch (paymentOption) { 70 | case "Credit Card": 71 | return ( 72 | 73 | 74 | 83 | 84 | 85 | You can check this integration using the credit card number{" "} 86 | 4242 4242 4242 4242 04 / 24 24 242 42424 87 | 88 | 89 | ); 90 | case "SEPA Direct Debit": 91 | return ( 92 | 93 | 94 | 105 | 106 | 107 | You can check this integration using the IBAN 108 |
109 | DE89370400440532013000 110 |
111 |
112 | ); 113 | default: 114 | throw new Error("No case selected in switch statement"); 115 | } 116 | }; 117 | 118 | return ( 119 | { 126 | event.preventDefault(); 127 | if (amount <= 0) { 128 | setAmountError("Can't be zero"); 129 | return; 130 | } 131 | if (stripeError) { 132 | setStripeError(""); 133 | } 134 | setLoading(true); 135 | const { error } = await stripe.createPaymentMethod( 136 | getStripePaymentInfo() 137 | ); 138 | if (error) { 139 | setStripeError(error.message); 140 | setLoading(false); 141 | return; 142 | } 143 | onSuccess(); 144 | }} 145 | content={ 146 | 147 | 148 | 149 | {paymentOptions.map(option => ( 150 | 151 | { 157 | setStripeError(""); 158 | setPaymentOption(option); 159 | }} 160 | color={theme.palette.common.black} 161 | > 162 | {option} 163 | 164 | 165 | ))} 166 | 167 | 168 | {renderPaymentComponent()} 169 | 170 | } 171 | actions={ 172 | 173 | 183 | 184 | } 185 | /> 186 | ); 187 | }); 188 | 189 | AddBalanceDialog.propTypes = { 190 | open: PropTypes.bool.isRequired, 191 | theme: PropTypes.object.isRequired, 192 | onClose: PropTypes.func.isRequired, 193 | onSuccess: PropTypes.func.isRequired 194 | }; 195 | 196 | function Wrapper(props) { 197 | const { open, onClose, onSuccess } = props; 198 | return ( 199 | 200 | {open && ( 201 | 202 | )} 203 | 204 | ); 205 | } 206 | 207 | 208 | Wrapper.propTypes = { 209 | open: PropTypes.bool.isRequired, 210 | onClose: PropTypes.func.isRequired, 211 | onSuccess: PropTypes.func.isRequired 212 | }; 213 | 214 | export default Wrapper; 215 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/LazyLoadAddBalanceDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | function LazyLoadAddBalanceDialog(props) { 5 | const { open, onClose, onSuccess } = props; 6 | const [AddBalanceDialog, setAddBalanceDialog] = useState(null); 7 | const [hasFetchedAddBalanceDialog, setHasFetchedAddBlanceDialog] = useState(false); 8 | 9 | useEffect(() => { 10 | if (open && !hasFetchedAddBalanceDialog) { 11 | setHasFetchedAddBlanceDialog(true); 12 | import("./AddBalanceDialog").then(Component => { 13 | setAddBalanceDialog(() => Component.default); 14 | }); 15 | } 16 | }, [open, hasFetchedAddBalanceDialog, setHasFetchedAddBlanceDialog, setAddBalanceDialog]); 17 | 18 | return ( 19 | 20 | {AddBalanceDialog && ( 21 | 26 | )} 27 | 28 | ); 29 | 30 | } 31 | 32 | LazyLoadAddBalanceDialog.propTypes = { 33 | open: PropTypes.bool.isRequired, 34 | onClose: PropTypes.func.isRequired, 35 | onSuccess: PropTypes.func.isRequired 36 | }; 37 | 38 | export default LazyLoadAddBalanceDialog; 39 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/Subscription.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { List, Divider, Paper } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import SubscriptionTable from "./SubscriptionTable"; 6 | import SubscriptionInfo from "./SubscriptionInfo"; 7 | 8 | const styles = { 9 | divider: { 10 | backgroundColor: "rgba(0, 0, 0, 0.26)" 11 | } 12 | }; 13 | 14 | function Subscription(props) { 15 | const { 16 | transactions, 17 | classes, 18 | openAddBalanceDialog, 19 | selectSubscription 20 | } = props; 21 | 22 | useEffect(selectSubscription, [selectSubscription]); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | Subscription.propTypes = { 36 | classes: PropTypes.object.isRequired, 37 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired, 38 | selectSubscription: PropTypes.func.isRequired, 39 | openAddBalanceDialog: PropTypes.func.isRequired 40 | }; 41 | 42 | export default withStyles(styles)(Subscription); 43 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/SubscriptionInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { ListItemText, Button, Toolbar } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = { 8 | toolbar: { 9 | justifyContent: "space-between" 10 | } 11 | }; 12 | 13 | function SubscriptionInfo(props) { 14 | const { classes, openAddBalanceDialog } = props; 15 | return ( 16 | 17 | 18 | 26 | 27 | ); 28 | } 29 | 30 | SubscriptionInfo.propTypes = { 31 | classes: PropTypes.object.isRequired, 32 | openAddBalanceDialog: PropTypes.func.isRequired 33 | }; 34 | 35 | export default withStyles(styles)(SubscriptionInfo); 36 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/SubscriptionTable.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Table, TableBody, TableCell, TablePagination, TableRow } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import EnhancedTableHead from "../../../shared/components/EnhancedTableHead"; 6 | import ColorfulChip from "../../../shared/components/ColorfulChip"; 7 | import unixToDateString from "../../../shared/functions/unixToDateString"; 8 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 9 | import currencyPrettyPrint from "../../../shared/functions/currencyPrettyPrint"; 10 | 11 | const styles = theme => ({ 12 | tableWrapper: { 13 | overflowX: "auto", 14 | width: "100%" 15 | }, 16 | blackBackground: { 17 | backgroundColor: theme.palette.primary.main 18 | }, 19 | contentWrapper: { 20 | padding: theme.spacing(3), 21 | [theme.breakpoints.down('sm')]: { 22 | padding: theme.spacing(2) 23 | }, 24 | width: "100%" 25 | }, 26 | dBlock: { 27 | display: "block !important" 28 | }, 29 | dNone: { 30 | display: "none !important" 31 | }, 32 | firstData: { 33 | paddingLeft: theme.spacing(3) 34 | } 35 | }); 36 | 37 | const rows = [ 38 | { 39 | id: "description", 40 | numeric: false, 41 | label: "Action" 42 | }, 43 | { 44 | id: "balanceChange", 45 | numeric: false, 46 | label: "Balance change" 47 | }, 48 | { 49 | id: "date", 50 | numeric: false, 51 | label: "Date" 52 | }, 53 | { 54 | id: "paidUntil", 55 | numeric: false, 56 | label: "Paid until" 57 | } 58 | ]; 59 | 60 | const rowsPerPage = 25; 61 | 62 | function SubscriptionTable(props) { 63 | const { transactions, theme, classes } = props; 64 | const [page, setPage] = useState(0); 65 | 66 | const handleChangePage = useCallback( 67 | (_, page) => { 68 | setPage(page); 69 | }, 70 | [setPage] 71 | ); 72 | 73 | if (transactions.length > 0) { 74 | return ( 75 |
76 | 77 | 78 | 79 | {transactions 80 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 81 | .map((transaction, index) => ( 82 | 83 | 88 | {transaction.description} 89 | 90 | 91 | {transaction.balanceChange > 0 ? ( 92 | 98 | ) : ( 99 | 103 | )} 104 | 105 | 106 | {unixToDateString(transaction.timestamp)} 107 | 108 | 109 | {transaction.paidUntil 110 | ? unixToDateString(transaction.paidUntil) 111 | : ""} 112 | 113 | 114 | ))} 115 | 116 |
117 | 0 ? classes.dBlock : classes.dNone, 133 | caption: transactions.length > 0 ? classes.dBlock : classes.dNone 134 | }} 135 | labelRowsPerPage="" 136 | /> 137 |
138 | ); 139 | } 140 | return ( 141 |
142 | 143 | No transactions received yet. 144 | 145 |
146 | ); 147 | } 148 | 149 | SubscriptionTable.propTypes = { 150 | theme: PropTypes.object.isRequired, 151 | classes: PropTypes.object.isRequired, 152 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired 153 | }; 154 | 155 | export default withStyles(styles, { withTheme: true })(SubscriptionTable); 156 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/stripe/StripeCardForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { TextField, Grid, InputAdornment } from "@mui/material"; 4 | import { CardElement } from "@stripe/react-stripe-js"; 5 | import StripeTextField from "./StripeTextField"; 6 | 7 | function StripeCardForm(props) { 8 | const { 9 | stripeError, 10 | setStripeError, 11 | amount, 12 | amountError, 13 | onAmountChange, 14 | name, 15 | setName 16 | } = props; 17 | return ( 18 | 19 | 20 | { 27 | setName(event.target.value); 28 | }} 29 | fullWidth 30 | autoFocus 31 | autoComplete="off" 32 | type="text" 33 | /> 34 | 35 | 36 | { 40 | onAmountChange(parseInt(event.target.value)); 41 | }} 42 | error={amountError ? true : false} 43 | helperText={amountError} 44 | variant="outlined" 45 | fullWidth 46 | type="number" 47 | margin="none" 48 | label="Amount" 49 | InputProps={{ 50 | startAdornment: $ 51 | }} 52 | /> 53 | 54 | 55 | { 65 | if (stripeError) { 66 | setStripeError(""); 67 | } 68 | }} 69 | > 70 | 71 | 72 | ); 73 | } 74 | 75 | StripeCardForm.propTypes = { 76 | stripeError: PropTypes.string.isRequired, 77 | setStripeError: PropTypes.func.isRequired, 78 | amount: PropTypes.number.isRequired, 79 | onAmountChange: PropTypes.func.isRequired, 80 | amountError: PropTypes.string.isRequired, 81 | name: PropTypes.string.isRequired, 82 | setName: PropTypes.func.isRequired 83 | }; 84 | 85 | export default StripeCardForm; 86 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/stripe/StripeIBANForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { TextField, Grid, InputAdornment } from "@mui/material"; 4 | import StripeTextField from "./StripeTextField"; 5 | import { IbanElement } from "@stripe/react-stripe-js"; 6 | 7 | function StripeIBANForm(props) { 8 | const { 9 | stripeError, 10 | setStripeError, 11 | amount, 12 | amountError, 13 | onAmountChange, 14 | name, 15 | setName, 16 | email, 17 | setEmail 18 | } = props; 19 | return ( 20 | 21 | 22 | { 29 | setName(event.target.value); 30 | }} 31 | fullWidth 32 | autoFocus 33 | autoComplete="off" 34 | type="text" 35 | /> 36 | 37 | 38 | { 42 | onAmountChange(parseInt(event.target.value)); 43 | }} 44 | error={amountError ? true : false} 45 | helperText={amountError} 46 | variant="outlined" 47 | fullWidth 48 | type="number" 49 | margin="none" 50 | label="Amount" 51 | InputProps={{ 52 | startAdornment: $ 53 | }} 54 | /> 55 | 56 | 57 | { 63 | setEmail(event.target.value); 64 | }} 65 | type="email" 66 | margin="none" 67 | label="Email" 68 | /> 69 | 70 | 71 | { 82 | if (stripeError) { 83 | setStripeError(""); 84 | } 85 | }} 86 | > 87 | 88 | 89 | ); 90 | } 91 | 92 | StripeIBANForm.propTypes = { 93 | stripeError: PropTypes.string.isRequired, 94 | setStripeError: PropTypes.func.isRequired, 95 | amount: PropTypes.number.isRequired, 96 | onAmountChange: PropTypes.func.isRequired, 97 | amountError: PropTypes.string.isRequired, 98 | name: PropTypes.string.isRequired, 99 | setName: PropTypes.func.isRequired, 100 | email: PropTypes.string.isRequired, 101 | setEmail: PropTypes.func.isRequired 102 | }; 103 | 104 | export default StripeIBANForm; 105 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/stripe/StripeTextField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField } from "@mui/material"; 3 | import withTheme from "@mui/styles/withTheme"; 4 | 5 | function StripeTextField(props) { 6 | const { stripeOptions, StripeElement, select, theme, ...rest } = props; 7 | const options = { 8 | style: { 9 | base: { 10 | ...theme.typography.body1, 11 | color: theme.palette.text.primary, 12 | fontSize: "16px", 13 | fontSmoothing: "antialiased", 14 | "::placeholder": { 15 | color: theme.palette.text.secondary, 16 | }, 17 | }, 18 | invalid: { 19 | iconColor: theme.palette.error.main, 20 | color: theme.palette.error.main, 21 | }, 22 | }, 23 | ...stripeOptions, 24 | }; 25 | return ( 26 | 33 | ); 34 | } 35 | 36 | export default withTheme(StripeTextField); 37 | -------------------------------------------------------------------------------- /src/logged_in/dummy_data/persons.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | src: `${process.env.PUBLIC_URL}/images/logged_in/image1.jpg`, 4 | name: "Markus", 5 | }, 6 | { 7 | src: `${process.env.PUBLIC_URL}/images/logged_in/image2.jpg`, 8 | name: "David", 9 | }, 10 | { 11 | src: `${process.env.PUBLIC_URL}/images/logged_in/image3.jpg`, 12 | name: "Arold", 13 | }, 14 | { 15 | src: `${process.env.PUBLIC_URL}/images/logged_in/image4.jpg`, 16 | name: "Joanic", 17 | }, 18 | { 19 | src: `${process.env.PUBLIC_URL}/images/logged_in/image5.jpg`, 20 | name: "Sophia", 21 | }, 22 | { 23 | src: `${process.env.PUBLIC_URL}/images/logged_in/image6.jpg`, 24 | name: "Aaron", 25 | }, 26 | { 27 | src: `${process.env.PUBLIC_URL}/images/logged_in/image7.jpg`, 28 | name: "Steven", 29 | }, 30 | { 31 | src: `${process.env.PUBLIC_URL}/images/logged_in/image8.jpg`, 32 | name: "Felix", 33 | }, 34 | { 35 | src: `${process.env.PUBLIC_URL}/images/logged_in/image9.jpg`, 36 | name: "Vivien", 37 | }, 38 | { 39 | src: `${process.env.PUBLIC_URL}/images/logged_in/image10.jpg`, 40 | name: "Leonie", 41 | }, 42 | ]; 43 | 44 | export default data; -------------------------------------------------------------------------------- /src/logged_out/components/Main.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import AOS from "aos/dist/aos"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import NavBar from "./navigation/NavBar"; 6 | import Footer from "./footer/Footer"; 7 | import "aos/dist/aos.css"; 8 | import CookieRulesDialog from "./cookies/CookieRulesDialog"; 9 | import CookieConsent from "./cookies/CookieConsent"; 10 | import dummyBlogPosts from "../dummy_data/blogPosts"; 11 | import DialogSelector from "./register_login/DialogSelector"; 12 | import Routing from "./Routing"; 13 | import smoothScrollTop from "../../shared/functions/smoothScrollTop"; 14 | 15 | AOS.init({ once: true }); 16 | 17 | const styles = (theme) => ({ 18 | wrapper: { 19 | backgroundColor: theme.palette.common.white, 20 | overflowX: "hidden", 21 | }, 22 | }); 23 | 24 | function Main(props) { 25 | const { classes } = props; 26 | const [selectedTab, setSelectedTab] = useState(null); 27 | const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false); 28 | const [blogPosts, setBlogPosts] = useState([]); 29 | const [dialogOpen, setDialogOpen] = useState(null); 30 | const [isCookieRulesDialogOpen, setIsCookieRulesDialogOpen] = useState(false); 31 | 32 | const selectHome = useCallback(() => { 33 | smoothScrollTop(); 34 | document.title = 35 | "WaVer - Free template for building a SaaS or admin application"; 36 | setSelectedTab("Home"); 37 | }, [setSelectedTab]); 38 | 39 | const selectBlog = useCallback(() => { 40 | smoothScrollTop(); 41 | document.title = "WaVer - Blog"; 42 | setSelectedTab("Blog"); 43 | }, [setSelectedTab]); 44 | 45 | const openLoginDialog = useCallback(() => { 46 | setDialogOpen("login"); 47 | setIsMobileDrawerOpen(false); 48 | }, [setDialogOpen, setIsMobileDrawerOpen]); 49 | 50 | const closeDialog = useCallback(() => { 51 | setDialogOpen(null); 52 | }, [setDialogOpen]); 53 | 54 | const openRegisterDialog = useCallback(() => { 55 | setDialogOpen("register"); 56 | setIsMobileDrawerOpen(false); 57 | }, [setDialogOpen, setIsMobileDrawerOpen]); 58 | 59 | const openTermsDialog = useCallback(() => { 60 | setDialogOpen("termsOfService"); 61 | }, [setDialogOpen]); 62 | 63 | const handleMobileDrawerOpen = useCallback(() => { 64 | setIsMobileDrawerOpen(true); 65 | }, [setIsMobileDrawerOpen]); 66 | 67 | const handleMobileDrawerClose = useCallback(() => { 68 | setIsMobileDrawerOpen(false); 69 | }, [setIsMobileDrawerOpen]); 70 | 71 | const openChangePasswordDialog = useCallback(() => { 72 | setDialogOpen("changePassword"); 73 | }, [setDialogOpen]); 74 | 75 | const fetchBlogPosts = useCallback(() => { 76 | const blogPosts = dummyBlogPosts.map((blogPost) => { 77 | let title = blogPost.title; 78 | title = title.toLowerCase(); 79 | /* Remove unwanted characters, only accept alphanumeric and space */ 80 | title = title.replace(/[^A-Za-z0-9 ]/g, ""); 81 | /* Replace multi spaces with a single space */ 82 | title = title.replace(/\s{2,}/g, " "); 83 | /* Replace space with a '-' symbol */ 84 | title = title.replace(/\s/g, "-"); 85 | blogPost.url = `/blog/post/${title}`; 86 | blogPost.params = `?id=${blogPost.id}`; 87 | return blogPost; 88 | }); 89 | setBlogPosts(blogPosts); 90 | }, [setBlogPosts]); 91 | 92 | const handleCookieRulesDialogOpen = useCallback(() => { 93 | setIsCookieRulesDialogOpen(true); 94 | }, [setIsCookieRulesDialogOpen]); 95 | 96 | const handleCookieRulesDialogClose = useCallback(() => { 97 | setIsCookieRulesDialogOpen(false); 98 | }, [setIsCookieRulesDialogOpen]); 99 | 100 | useEffect(fetchBlogPosts, [fetchBlogPosts]); 101 | 102 | return ( 103 |
104 | {!isCookieRulesDialogOpen && ( 105 | 108 | )} 109 | 117 | 121 | 130 | 135 |
136 |
137 | ); 138 | } 139 | 140 | Main.propTypes = { 141 | classes: PropTypes.object.isRequired, 142 | }; 143 | 144 | export default withStyles(styles, { withTheme: true })(memo(Main)); 145 | -------------------------------------------------------------------------------- /src/logged_out/components/Routing.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Switch } from "react-router-dom"; 4 | import PropsRoute from "../../shared/components/PropsRoute"; 5 | import Home from "./home/Home"; 6 | import Blog from "./blog/Blog"; 7 | import BlogPost from "./blog/BlogPost"; 8 | import useLocationBlocker from "../../shared/functions/useLocationBlocker"; 9 | 10 | function Routing(props) { 11 | const { blogPosts, selectBlog, selectHome } = props; 12 | useLocationBlocker(); 13 | return ( 14 | 15 | {blogPosts.map((post) => ( 16 | blogPost.id !== post.id 26 | )} 27 | /> 28 | ))} 29 | 36 | 37 | 38 | ); 39 | } 40 | 41 | Routing.propTypes = { 42 | blogposts: PropTypes.arrayOf(PropTypes.object), 43 | selectHome: PropTypes.func.isRequired, 44 | selectBlog: PropTypes.func.isRequired, 45 | }; 46 | 47 | export default memo(Routing); 48 | -------------------------------------------------------------------------------- /src/logged_out/components/blog/Blog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { Grid, Box } from "@mui/material"; 5 | import withStyles from "@mui/styles/withStyles"; 6 | import BlogCard from "./BlogCard"; 7 | import useMediaQuery from "@mui/material/useMediaQuery"; 8 | 9 | const styles = (theme) => ({ 10 | blogContentWrapper: { 11 | marginLeft: theme.spacing(1), 12 | marginRight: theme.spacing(1), 13 | [theme.breakpoints.up("sm")]: { 14 | marginLeft: theme.spacing(4), 15 | marginRight: theme.spacing(4), 16 | }, 17 | maxWidth: 1280, 18 | width: "100%", 19 | }, 20 | wrapper: { 21 | minHeight: "60vh", 22 | }, 23 | noDecoration: { 24 | textDecoration: "none !important", 25 | }, 26 | }); 27 | 28 | function getVerticalBlogPosts(isWidthUpSm, isWidthUpMd, blogPosts) { 29 | const gridRows = [[], [], []]; 30 | let rows; 31 | let xs; 32 | if (isWidthUpMd) { 33 | rows = 3; 34 | xs = 4; 35 | } else if (isWidthUpSm) { 36 | rows = 2; 37 | xs = 6; 38 | } else { 39 | rows = 1; 40 | xs = 12; 41 | } 42 | blogPosts.forEach((blogPost, index) => { 43 | gridRows[index % rows].push( 44 | 45 | 46 | 53 | 54 | 55 | ); 56 | }); 57 | return gridRows.map((element, index) => ( 58 | 59 | {element} 60 | 61 | )); 62 | } 63 | 64 | function Blog(props) { 65 | const { classes, blogPosts, selectBlog, theme } = props; 66 | 67 | const isWidthUpSm = useMediaQuery(theme.breakpoints.up("sm")); 68 | const isWidthUpMd = useMediaQuery(theme.breakpoints.up("md")); 69 | 70 | useEffect(() => { 71 | selectBlog(); 72 | }, [selectBlog]); 73 | 74 | return ( 75 | 80 |
81 | 82 | {getVerticalBlogPosts(isWidthUpSm, isWidthUpMd, blogPosts)} 83 | 84 |
85 |
86 | ); 87 | } 88 | 89 | Blog.propTypes = { 90 | selectBlog: PropTypes.func.isRequired, 91 | classes: PropTypes.object.isRequired, 92 | blogPosts: PropTypes.arrayOf(PropTypes.object), 93 | }; 94 | 95 | export default withStyles(styles, { withTheme: true })(Blog); 96 | -------------------------------------------------------------------------------- /src/logged_out/components/blog/BlogCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import format from "date-fns/format"; 5 | import classNames from "classnames"; 6 | import { Typography, Card, Box } from "@mui/material"; 7 | 8 | import withStyles from '@mui/styles/withStyles'; 9 | 10 | const styles = (theme) => ({ 11 | img: { 12 | width: "100%", 13 | height: "auto", 14 | marginBottom: 8, 15 | }, 16 | card: { 17 | boxShadow: theme.shadows[2], 18 | }, 19 | noDecoration: { 20 | textDecoration: "none !important", 21 | }, 22 | title: { 23 | transition: theme.transitions.create(["background-color"], { 24 | duration: theme.transitions.duration.complex, 25 | easing: theme.transitions.easing.easeInOut, 26 | }), 27 | cursor: "pointer", 28 | color: theme.palette.secondary.main, 29 | "&:hover": { 30 | color: theme.palette.secondary.dark, 31 | }, 32 | "&:active": { 33 | color: theme.palette.primary.dark, 34 | }, 35 | }, 36 | link: { 37 | transition: theme.transitions.create(["background-color"], { 38 | duration: theme.transitions.duration.complex, 39 | easing: theme.transitions.easing.easeInOut, 40 | }), 41 | cursor: "pointer", 42 | color: theme.palette.primary.main, 43 | "&:hover": { 44 | color: theme.palette.primary.dark, 45 | }, 46 | }, 47 | showFocus: { 48 | "&:focus span": { 49 | color: theme.palette.secondary.dark, 50 | }, 51 | }, 52 | }); 53 | 54 | function BlogCard(props) { 55 | const { classes, url, src, date, title, snippet } = props; 56 | 57 | return ( 58 | 59 | {src && ( 60 | 61 | 62 | 63 | )} 64 | 65 | 66 | {format(new Date(date * 1000), "PPP", { 67 | awareOfUnicodeTokens: true, 68 | })} 69 | 70 | 74 | 75 | {title} 76 | 77 | 78 | 79 | {snippet} 80 | 81 | read more... 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | 89 | BlogCard.propTypes = { 90 | classes: PropTypes.object.isRequired, 91 | url: PropTypes.string.isRequired, 92 | title: PropTypes.string.isRequired, 93 | date: PropTypes.number.isRequired, 94 | snippet: PropTypes.string.isRequired, 95 | src: PropTypes.string, 96 | }; 97 | 98 | export default withStyles(styles, { withTheme: true })(BlogCard); 99 | -------------------------------------------------------------------------------- /src/logged_out/components/blog/BlogPost.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import format from "date-fns/format"; 5 | import { Grid, Typography, Card, Box } from "@mui/material"; 6 | import withStyles from '@mui/styles/withStyles'; 7 | import BlogCard from "./BlogCard"; 8 | import ShareButton from "../../../shared/components/ShareButton"; 9 | import ZoomImage from "../../../shared/components/ZoomImage"; 10 | import smoothScrollTop from "../../../shared/functions/smoothScrollTop"; 11 | 12 | const styles = (theme) => ({ 13 | blogContentWrapper: { 14 | marginLeft: theme.spacing(1), 15 | marginRight: theme.spacing(1), 16 | [theme.breakpoints.up("sm")]: { 17 | marginLeft: theme.spacing(4), 18 | marginRight: theme.spacing(4), 19 | }, 20 | maxWidth: 1280, 21 | width: "100%", 22 | }, 23 | wrapper: { 24 | minHeight: "60vh", 25 | }, 26 | img: { 27 | width: "100%", 28 | height: "auto", 29 | }, 30 | card: { 31 | boxShadow: theme.shadows[4], 32 | }, 33 | }); 34 | 35 | function BlogPost(props) { 36 | const { classes, date, title, src, content, otherArticles } = props; 37 | 38 | useEffect(() => { 39 | document.title = `WaVer - ${title}`; 40 | smoothScrollTop(); 41 | }, [title]); 42 | 43 | return ( 44 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | {title} 56 | 57 | 58 | {format(new Date(date * 1000), "PPP", { 59 | awareOfUnicodeTokens: true, 60 | })} 61 | 62 | 63 | 64 | 65 | {content} 66 | 67 | 68 | {["Facebook", "Twitter", "Reddit", "Tumblr"].map( 69 | (type, index) => ( 70 | 71 | 82 | 83 | ) 84 | )} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Other articles 93 | 94 | {otherArticles.map((blogPost) => ( 95 | 96 | 102 | 103 | ))} 104 | 105 | 106 |
107 |
108 | ); 109 | } 110 | 111 | BlogPost.propTypes = { 112 | classes: PropTypes.object.isRequired, 113 | title: PropTypes.string.isRequired, 114 | date: PropTypes.number.isRequired, 115 | src: PropTypes.string.isRequired, 116 | content: PropTypes.node.isRequired, 117 | otherArticles: PropTypes.arrayOf(PropTypes.object).isRequired, 118 | }; 119 | 120 | export default withStyles(styles, { withTheme: true })(BlogPost); 121 | -------------------------------------------------------------------------------- /src/logged_out/components/cookies/CookieConsent.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Cookies from "js-cookie"; 4 | import { Snackbar, Button, Typography, Box } from "@mui/material"; 5 | import withStyles from '@mui/styles/withStyles'; 6 | import fetchIpData from "./fetchIpData"; 7 | 8 | const styles = (theme) => ({ 9 | snackbarContent: { 10 | borderBottomLeftRadius: 0, 11 | borderBottomRightRadius: 0, 12 | paddingLeft: theme.spacing(3), 13 | paddingRight: theme.spacing(3), 14 | }, 15 | }); 16 | 17 | const europeanCountryCodes = [ 18 | "AT", 19 | "BE", 20 | "BG", 21 | "CY", 22 | "CZ", 23 | "DE", 24 | "DK", 25 | "EE", 26 | "ES", 27 | "FI", 28 | "FR", 29 | "GB", 30 | "GR", 31 | "HR", 32 | "HU", 33 | "IE", 34 | "IT", 35 | "LT", 36 | "LU", 37 | "LV", 38 | "MT", 39 | "NL", 40 | "PO", 41 | "PT", 42 | "RO", 43 | "SE", 44 | "SI", 45 | "SK", 46 | ]; 47 | 48 | function CookieConsent(props) { 49 | const { classes, handleCookieRulesDialogOpen } = props; 50 | const [isVisible, setIsVisible] = useState(false); 51 | 52 | const openOnEuCountry = useCallback(() => { 53 | fetchIpData 54 | .then((data) => { 55 | if ( 56 | data && 57 | data.country && 58 | !europeanCountryCodes.includes(data.country) 59 | ) { 60 | setIsVisible(false); 61 | } else { 62 | setIsVisible(true); 63 | } 64 | }) 65 | .catch(() => { 66 | setIsVisible(true); 67 | }); 68 | }, [setIsVisible]); 69 | 70 | /** 71 | * Set a persistent cookie 72 | */ 73 | const onAccept = useCallback(() => { 74 | Cookies.set("remember-cookie-snackbar", "", { 75 | expires: 365, 76 | }); 77 | setIsVisible(false); 78 | }, [setIsVisible]); 79 | 80 | useEffect(() => { 81 | if (Cookies.get("remember-cookie-snackbar") === undefined) { 82 | openOnEuCountry(); 83 | } 84 | }, [openOnEuCountry]); 85 | 86 | return ( 87 | 92 | We use cookies to ensure you get the best experience on our website.{" "} 93 | 94 | } 95 | action={ 96 | 97 | 98 | 101 | 102 | 105 | 106 | } 107 | /> 108 | ); 109 | } 110 | 111 | CookieConsent.propTypes = { 112 | handleCookieRulesDialogOpen: PropTypes.func.isRequired, 113 | }; 114 | 115 | export default withStyles(styles, { withTheme: true })(CookieConsent); 116 | -------------------------------------------------------------------------------- /src/logged_out/components/cookies/fetchIpData.js: -------------------------------------------------------------------------------- 1 | const fetchIpData = new Promise((resolve, reject) => { 2 | const ajax = new XMLHttpRequest(); 3 | if (window.location.href.includes("localhost")) { 4 | /** 5 | * Resolve with dummydata, GET call will be rejected, 6 | * since ipinfos server is configured that way 7 | */ 8 | resolve({ data: { country: "DE" } }); 9 | return; 10 | } 11 | ajax.open("GET", "https://ipinfo.io/json"); 12 | ajax.onload = () => { 13 | const response = JSON.parse(ajax.responseText); 14 | if (response) { 15 | resolve(response); 16 | } else { 17 | reject(); 18 | } 19 | }; 20 | ajax.onerror = reject; 21 | ajax.send(); 22 | }); 23 | 24 | export default fetchIpData; 25 | -------------------------------------------------------------------------------- /src/logged_out/components/home/FeatureCard.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Typography } from "@mui/material"; 4 | 5 | import withStyles from "@mui/styles/withStyles"; 6 | 7 | const styles = (theme) => ({ 8 | iconWrapper: { 9 | borderRadius: theme.shape.borderRadius, 10 | textAlign: "center", 11 | display: "inline-flex", 12 | alignItems: "center", 13 | justifyContent: "center", 14 | marginBottom: theme.spacing(3), 15 | padding: theme.spacing(1) * 1.5, 16 | }, 17 | }); 18 | 19 | function shadeColor(hex, percent) { 20 | const f = parseInt(hex.slice(1), 16); 21 | 22 | const t = percent < 0 ? 0 : 255; 23 | 24 | const p = percent < 0 ? percent * -1 : percent; 25 | 26 | const R = f >> 16; 27 | 28 | const G = (f >> 8) & 0x00ff; 29 | 30 | const B = f & 0x0000ff; 31 | return `#${( 32 | 0x1000000 + 33 | (Math.round((t - R) * p) + R) * 0x10000 + 34 | (Math.round((t - G) * p) + G) * 0x100 + 35 | (Math.round((t - B) * p) + B) 36 | ) 37 | .toString(16) 38 | .slice(1)}`; 39 | } 40 | 41 | function FeatureCard(props) { 42 | const { classes, Icon, color, headline, text } = props; 43 | return ( 44 | 45 |
55 | {Icon} 56 |
57 | 58 | {headline} 59 | 60 | 61 | {text} 62 | 63 |
64 | ); 65 | } 66 | 67 | FeatureCard.propTypes = { 68 | classes: PropTypes.object.isRequired, 69 | Icon: PropTypes.element.isRequired, 70 | color: PropTypes.string.isRequired, 71 | headline: PropTypes.string.isRequired, 72 | text: PropTypes.string.isRequired, 73 | }; 74 | 75 | export default withStyles(styles, { withTheme: true })(FeatureCard); 76 | -------------------------------------------------------------------------------- /src/logged_out/components/home/FeatureSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid, Typography } from "@mui/material"; 3 | import CodeIcon from "@mui/icons-material/Code"; 4 | import BuildIcon from "@mui/icons-material/Build"; 5 | import ComputerIcon from "@mui/icons-material/Computer"; 6 | import BarChartIcon from "@mui/icons-material/BarChart"; 7 | import HeadsetMicIcon from "@mui/icons-material/HeadsetMic"; 8 | import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; 9 | import CloudIcon from "@mui/icons-material/Cloud"; 10 | import MeassageIcon from "@mui/icons-material/Message"; 11 | import CancelIcon from "@mui/icons-material/Cancel"; 12 | import calculateSpacing from "./calculateSpacing"; 13 | import useMediaQuery from "@mui/material/useMediaQuery"; 14 | import { withTheme } from "@mui/styles"; 15 | import FeatureCard from "./FeatureCard"; 16 | import useWidth from "../../../shared/functions/useWidth"; 17 | 18 | const iconSize = 30; 19 | 20 | const features = [ 21 | { 22 | color: "#00C853", 23 | headline: "Feature 1", 24 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 25 | icon: , 26 | mdDelay: "0", 27 | smDelay: "0", 28 | }, 29 | { 30 | color: "#6200EA", 31 | headline: "Feature 2", 32 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 33 | icon: , 34 | mdDelay: "200", 35 | smDelay: "200", 36 | }, 37 | { 38 | color: "#0091EA", 39 | headline: "Feature 3", 40 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 41 | icon: , 42 | mdDelay: "400", 43 | smDelay: "0", 44 | }, 45 | { 46 | color: "#d50000", 47 | headline: "Feature 4", 48 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 49 | icon: , 50 | mdDelay: "0", 51 | smDelay: "200", 52 | }, 53 | { 54 | color: "#DD2C00", 55 | headline: "Feature 5", 56 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 57 | icon: , 58 | mdDelay: "200", 59 | smDelay: "0", 60 | }, 61 | { 62 | color: "#64DD17", 63 | headline: "Feature 6", 64 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 65 | icon: , 66 | mdDelay: "400", 67 | smDelay: "200", 68 | }, 69 | { 70 | color: "#304FFE", 71 | headline: "Feature 7", 72 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 73 | icon: , 74 | mdDelay: "0", 75 | smDelay: "0", 76 | }, 77 | { 78 | color: "#C51162", 79 | headline: "Feature 8", 80 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 81 | icon: , 82 | mdDelay: "200", 83 | smDelay: "200", 84 | }, 85 | { 86 | color: "#00B8D4", 87 | headline: "Feature 9", 88 | text: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 89 | icon: , 90 | mdDelay: "400", 91 | smDelay: "0", 92 | }, 93 | ]; 94 | 95 | function FeatureSection(props) { 96 | const { theme } = props; 97 | const width = useWidth(); 98 | const isWidthUpMd = useMediaQuery(theme.breakpoints.up("md")); 99 | 100 | return ( 101 |
102 |
103 | 104 | Features 105 | 106 |
107 | 108 | {features.map((element) => ( 109 | 117 | 123 | 124 | ))} 125 | 126 |
127 |
128 |
129 | ); 130 | } 131 | 132 | FeatureSection.propTypes = {}; 133 | 134 | export default withTheme(FeatureSection); 135 | -------------------------------------------------------------------------------- /src/logged_out/components/home/HeadSection.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { Grid, Typography, Card, Button, Hidden, Box } from "@mui/material"; 5 | import withStyles from "@mui/styles/withStyles"; 6 | import WaveBorder from "../../../shared/components/WaveBorder"; 7 | import ZoomImage from "../../../shared/components/ZoomImage"; 8 | import useMediaQuery from "@mui/material/useMediaQuery"; 9 | 10 | const styles = (theme) => ({ 11 | extraLargeButtonLabel: { 12 | fontSize: theme.typography.body1.fontSize, 13 | [theme.breakpoints.up("sm")]: { 14 | fontSize: theme.typography.h6.fontSize, 15 | }, 16 | }, 17 | extraLargeButton: { 18 | paddingTop: theme.spacing(1.5), 19 | paddingBottom: theme.spacing(1.5), 20 | [theme.breakpoints.up("xs")]: { 21 | paddingTop: theme.spacing(1), 22 | paddingBottom: theme.spacing(1), 23 | }, 24 | [theme.breakpoints.up("lg")]: { 25 | paddingTop: theme.spacing(2), 26 | paddingBottom: theme.spacing(2), 27 | }, 28 | }, 29 | card: { 30 | boxShadow: theme.shadows[4], 31 | marginLeft: theme.spacing(2), 32 | marginRight: theme.spacing(2), 33 | [theme.breakpoints.up("xs")]: { 34 | paddingTop: theme.spacing(3), 35 | paddingBottom: theme.spacing(3), 36 | }, 37 | [theme.breakpoints.up("sm")]: { 38 | paddingTop: theme.spacing(5), 39 | paddingBottom: theme.spacing(5), 40 | paddingLeft: theme.spacing(4), 41 | paddingRight: theme.spacing(4), 42 | }, 43 | [theme.breakpoints.up("md")]: { 44 | paddingTop: theme.spacing(5.5), 45 | paddingBottom: theme.spacing(5.5), 46 | paddingLeft: theme.spacing(5), 47 | paddingRight: theme.spacing(5), 48 | }, 49 | [theme.breakpoints.up("lg")]: { 50 | paddingTop: theme.spacing(6), 51 | paddingBottom: theme.spacing(6), 52 | paddingLeft: theme.spacing(6), 53 | paddingRight: theme.spacing(6), 54 | }, 55 | [theme.breakpoints.down("xl")]: { 56 | width: "auto", 57 | }, 58 | }, 59 | wrapper: { 60 | position: "relative", 61 | backgroundColor: theme.palette.secondary.main, 62 | paddingBottom: theme.spacing(2), 63 | }, 64 | image: { 65 | maxWidth: "100%", 66 | verticalAlign: "middle", 67 | borderRadius: theme.shape.borderRadius, 68 | boxShadow: theme.shadows[4], 69 | }, 70 | container: { 71 | marginTop: theme.spacing(6), 72 | marginBottom: theme.spacing(12), 73 | [theme.breakpoints.down("lg")]: { 74 | marginBottom: theme.spacing(9), 75 | }, 76 | [theme.breakpoints.down("md")]: { 77 | marginBottom: theme.spacing(6), 78 | }, 79 | [theme.breakpoints.down("md")]: { 80 | marginBottom: theme.spacing(3), 81 | }, 82 | }, 83 | containerFix: { 84 | [theme.breakpoints.up("md")]: { 85 | maxWidth: "none !important", 86 | }, 87 | }, 88 | waveBorder: { 89 | paddingTop: theme.spacing(4), 90 | }, 91 | }); 92 | 93 | function HeadSection(props) { 94 | const { classes, theme } = props; 95 | const isWidthUpLg = useMediaQuery(theme.breakpoints.up("lg")); 96 | 97 | return ( 98 | 99 |
100 |
101 | 102 | 107 |
108 | 109 | 110 | 116 | 117 | 118 | Free Template for building a SaaS app using 119 | Material-UI 120 | 121 | 122 |
123 | 124 | 128 | Lorem ipsum dolor sit amet, consetetur sadipscing 129 | elitr, sed diam nonumy eirmod tempor invidunt 130 | 131 | 132 | 142 |
143 |
144 |
145 | 146 | 147 | 152 | 153 | 154 |
155 |
156 |
157 |
158 |
159 |
160 | 166 |
167 | ); 168 | } 169 | 170 | HeadSection.propTypes = { 171 | classes: PropTypes.object, 172 | theme: PropTypes.object, 173 | }; 174 | 175 | export default withStyles(styles, { withTheme: true })(HeadSection); 176 | -------------------------------------------------------------------------------- /src/logged_out/components/home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import HeadSection from "./HeadSection"; 4 | import FeatureSection from "./FeatureSection"; 5 | import PricingSection from "./PricingSection"; 6 | 7 | function Home(props) { 8 | const { selectHome } = props; 9 | useEffect(() => { 10 | selectHome(); 11 | }, [selectHome]); 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | Home.propTypes = { 22 | selectHome: PropTypes.func.isRequired 23 | }; 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /src/logged_out/components/home/PriceCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Typography, Box } from "@mui/material"; 4 | import withStyles from "@mui/styles/withStyles"; 5 | import CheckIcon from "@mui/icons-material/Check"; 6 | 7 | const styles = (theme) => ({ 8 | card: { 9 | paddingTop: theme.spacing(6), 10 | paddingBottom: theme.spacing(6), 11 | paddingLeft: theme.spacing(4), 12 | paddingRight: theme.spacing(4), 13 | marginTop: theme.spacing(2), 14 | border: `3px solid ${theme.palette.primary.dark}`, 15 | borderRadius: theme.shape.borderRadius * 2, 16 | }, 17 | cardHightlighted: { 18 | paddingTop: theme.spacing(8), 19 | paddingBottom: theme.spacing(4), 20 | paddingLeft: theme.spacing(4), 21 | paddingRight: theme.spacing(4), 22 | border: `3px solid ${theme.palette.primary.dark}`, 23 | borderRadius: theme.shape.borderRadius * 2, 24 | backgroundColor: theme.palette.primary.main, 25 | [theme.breakpoints.down("sm")]: { 26 | marginTop: theme.spacing(2), 27 | }, 28 | }, 29 | title: { 30 | color: theme.palette.primary.main, 31 | }, 32 | }); 33 | 34 | function PriceCard(props) { 35 | const { classes, theme, title, pricing, features, highlighted } = props; 36 | return ( 37 |
38 | 39 | 43 | {title} 44 | 45 | 46 | 47 | 51 | {pricing} 52 | 53 | 54 | {features.map((feature, index) => ( 55 | 56 | 63 | 64 | 68 | {feature} 69 | 70 | 71 | 72 | ))} 73 |
74 | ); 75 | } 76 | 77 | PriceCard.propTypes = { 78 | classes: PropTypes.object.isRequired, 79 | theme: PropTypes.object.isRequired, 80 | title: PropTypes.string.isRequired, 81 | pricing: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired, 82 | highlighted: PropTypes.bool, 83 | }; 84 | 85 | export default withStyles(styles, { withTheme: true })(PriceCard); 86 | -------------------------------------------------------------------------------- /src/logged_out/components/home/PricingSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | import { Grid, Typography } from "@mui/material"; 4 | import { withStyles } from "@mui/styles"; 5 | import PriceCard from "./PriceCard"; 6 | import calculateSpacing from "./calculateSpacing"; 7 | import useMediaQuery from "@mui/material/useMediaQuery"; 8 | import useWidth from "../../../shared/functions/useWidth"; 9 | 10 | const styles = (theme) => ({ 11 | containerFix: { 12 | [theme.breakpoints.down("lg")]: { 13 | paddingLeft: theme.spacing(6), 14 | paddingRight: theme.spacing(6), 15 | }, 16 | [theme.breakpoints.down("md")]: { 17 | paddingLeft: theme.spacing(4), 18 | paddingRight: theme.spacing(4), 19 | }, 20 | [theme.breakpoints.down("sm")]: { 21 | paddingLeft: theme.spacing(2), 22 | paddingRight: theme.spacing(2), 23 | }, 24 | overflow: "hidden", 25 | paddingTop: theme.spacing(1), 26 | paddingBottom: theme.spacing(1), 27 | }, 28 | cardWrapper: { 29 | [theme.breakpoints.down("sm")]: { 30 | marginLeft: "auto", 31 | marginRight: "auto", 32 | maxWidth: 340, 33 | }, 34 | }, 35 | cardWrapperHighlighted: { 36 | [theme.breakpoints.down("sm")]: { 37 | marginLeft: "auto", 38 | marginRight: "auto", 39 | maxWidth: 360, 40 | }, 41 | }, 42 | }); 43 | 44 | function PricingSection(props) { 45 | const { classes, theme } = props; 46 | const width = useWidth(); 47 | const isWidthUpMd = useMediaQuery(theme.breakpoints.up("md")); 48 | return ( 49 |
50 | 51 | Pricing 52 | 53 |
54 | 59 | 67 | 71 | $14.99 72 | / month 73 | 74 | } 75 | features={["Feature 1", "Feature 2", "Feature 3"]} 76 | /> 77 | 78 | 87 | 92 | $29.99 93 | / month 94 | 95 | } 96 | features={["Feature 1", "Feature 2", "Feature 3"]} 97 | /> 98 | 99 | 108 | 112 | $49.99 113 | / month 114 | 115 | } 116 | features={["Feature 1", "Feature 2", "Feature 3"]} 117 | /> 118 | 119 | 128 | 132 | $99.99 133 | / month 134 | 135 | } 136 | features={["Feature 1", "Feature 2", "Feature 3"]} 137 | /> 138 | 139 | 140 |
141 |
142 | ); 143 | } 144 | 145 | PricingSection.propTypes = {}; 146 | 147 | export default withStyles(styles, { withTheme: true })(PricingSection); 148 | -------------------------------------------------------------------------------- /src/logged_out/components/home/calculateSpacing.js: -------------------------------------------------------------------------------- 1 | function calculateSpacing(width, theme) { 2 | console.log(theme["breakpoints"]); 3 | const currentWidth = theme["breakpoints"]["values"][width]; 4 | if (currentWidth >= theme["breakpoints"]["values"]["lg"]) { 5 | return 10; 6 | } 7 | if (currentWidth >= theme["breakpoints"]["values"]["md"]) { 8 | return 8; 9 | } 10 | if (currentWidth >= theme["breakpoints"]["values"]["sm"]) { 11 | return 6; 12 | } 13 | return 4; 14 | } 15 | 16 | export default calculateSpacing; 17 | -------------------------------------------------------------------------------- /src/logged_out/components/navigation/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { AppBar, Toolbar, Typography, Button, Hidden, IconButton } from "@mui/material"; 5 | import withStyles from '@mui/styles/withStyles'; 6 | import MenuIcon from "@mui/icons-material/Menu"; 7 | import HomeIcon from "@mui/icons-material/Home"; 8 | import HowToRegIcon from "@mui/icons-material/HowToReg"; 9 | import LockOpenIcon from "@mui/icons-material/LockOpen"; 10 | import BookIcon from "@mui/icons-material/Book"; 11 | import NavigationDrawer from "../../../shared/components/NavigationDrawer"; 12 | 13 | const styles = theme => ({ 14 | appBar: { 15 | boxShadow: theme.shadows[6], 16 | backgroundColor: theme.palette.common.white 17 | }, 18 | toolbar: { 19 | display: "flex", 20 | justifyContent: "space-between" 21 | }, 22 | menuButtonText: { 23 | fontSize: theme.typography.body1.fontSize, 24 | fontWeight: theme.typography.h6.fontWeight 25 | }, 26 | brandText: { 27 | fontFamily: "'Baloo Bhaijaan', cursive", 28 | fontWeight: 400 29 | }, 30 | noDecoration: { 31 | textDecoration: "none !important" 32 | } 33 | }); 34 | 35 | function NavBar(props) { 36 | const { 37 | classes, 38 | openRegisterDialog, 39 | openLoginDialog, 40 | handleMobileDrawerOpen, 41 | handleMobileDrawerClose, 42 | mobileDrawerOpen, 43 | selectedTab 44 | } = props; 45 | const menuItems = [ 46 | { 47 | link: "/", 48 | name: "Home", 49 | icon: 50 | }, 51 | { 52 | link: "/blog", 53 | name: "Blog", 54 | icon: 55 | }, 56 | { 57 | name: "Register", 58 | onClick: openRegisterDialog, 59 | icon: 60 | }, 61 | { 62 | name: "Login", 63 | onClick: openLoginDialog, 64 | icon: 65 | } 66 | ]; 67 | return ( 68 |
69 | 70 | 71 |
72 | 78 | Wa 79 | 80 | 86 | Ver 87 | 88 |
89 |
90 | 91 | 96 | 97 | 98 | 99 | 100 | {menuItems.map(element => { 101 | if (element.link) { 102 | return ( 103 | 109 | 116 | 117 | ); 118 | } 119 | return ( 120 | 129 | ); 130 | })} 131 | 132 |
133 |
134 |
135 | 142 |
143 | ); 144 | } 145 | 146 | NavBar.propTypes = { 147 | classes: PropTypes.object.isRequired, 148 | handleMobileDrawerOpen: PropTypes.func, 149 | handleMobileDrawerClose: PropTypes.func, 150 | mobileDrawerOpen: PropTypes.bool, 151 | selectedTab: PropTypes.string, 152 | openRegisterDialog: PropTypes.func.isRequired, 153 | openLoginDialog: PropTypes.func.isRequired 154 | }; 155 | 156 | export default withStyles(styles, { withTheme: true })(memo(NavBar)); 157 | -------------------------------------------------------------------------------- /src/logged_out/components/register_login/ChangePasswordDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { TextField, Dialog, DialogContent, DialogActions, Button, Typography } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 6 | 7 | const styles = (theme) => ({ 8 | dialogContent: { 9 | paddingTop: theme.spacing(2), 10 | }, 11 | dialogActions: { 12 | paddingTop: theme.spacing(2), 13 | paddingBottom: theme.spacing(2), 14 | paddingRight: theme.spacing(2), 15 | }, 16 | }); 17 | 18 | function ChangePassword(props) { 19 | const { onClose, classes, setLoginStatus } = props; 20 | const [isLoading, setIsLoading] = useState(false); 21 | 22 | const sendPasswordEmail = useCallback(() => { 23 | setIsLoading(true); 24 | setTimeout(() => { 25 | setLoginStatus("verificationEmailSend"); 26 | setIsLoading(false); 27 | onClose(); 28 | }, 1500); 29 | }, [setIsLoading, setLoginStatus, onClose]); 30 | 31 | return ( 32 | 38 |
{ 40 | e.preventDefault(); 41 | sendPasswordEmail(); 42 | }} 43 | > 44 | 45 | 46 | Enter your email address below and we will send you instructions on 47 | how to reset your password. 48 | 49 | 59 | 60 | 61 | 64 | 73 | 74 |
75 |
76 | ); 77 | } 78 | 79 | ChangePassword.propTypes = { 80 | onClose: PropTypes.func.isRequired, 81 | classes: PropTypes.object.isRequired, 82 | theme: PropTypes.object.isRequired, 83 | setLoginStatus: PropTypes.func.isRequired, 84 | }; 85 | 86 | export default withStyles(styles, { withTheme: true })(ChangePassword); 87 | -------------------------------------------------------------------------------- /src/logged_out/components/register_login/DialogSelector.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import RegisterDialog from "./RegisterDialog"; 4 | import TermsOfServiceDialog from "./TermsOfServiceDialog"; 5 | import LoginDialog from "./LoginDialog"; 6 | import ChangePasswordDialog from "./ChangePasswordDialog"; 7 | import ModalBackdrop from "../../../shared/components/ModalBackdrop"; 8 | 9 | function DialogSelector(props) { 10 | const { 11 | dialogOpen, 12 | openTermsDialog, 13 | openRegisterDialog, 14 | openLoginDialog, 15 | openChangePasswordDialog, 16 | onClose, 17 | } = props; 18 | const [loginStatus, setLoginStatus] = useState(null); 19 | const [registerStatus, setRegisterStatus] = useState(null); 20 | 21 | const _onClose = useCallback(() => { 22 | setLoginStatus(null); 23 | setRegisterStatus(null); 24 | onClose(); 25 | }, [onClose, setLoginStatus, setRegisterStatus]); 26 | 27 | const printDialog = useCallback(() => { 28 | switch (dialogOpen) { 29 | case "register": 30 | return ( 31 | 37 | ); 38 | case "termsOfService": 39 | return ; 40 | case "login": 41 | return ( 42 | 48 | ); 49 | case "changePassword": 50 | return ( 51 | 55 | ); 56 | default: 57 | } 58 | }, [ 59 | dialogOpen, 60 | openChangePasswordDialog, 61 | openLoginDialog, 62 | openRegisterDialog, 63 | openTermsDialog, 64 | _onClose, 65 | loginStatus, 66 | registerStatus, 67 | setLoginStatus, 68 | setRegisterStatus, 69 | ]); 70 | 71 | return ( 72 | 73 | {dialogOpen && } 74 | {printDialog()} 75 | 76 | ); 77 | } 78 | 79 | DialogSelector.propTypes = { 80 | dialogOpen: PropTypes.string, 81 | openLoginDialog: PropTypes.func.isRequired, 82 | onClose: PropTypes.func.isRequired, 83 | openTermsDialog: PropTypes.func.isRequired, 84 | openRegisterDialog: PropTypes.func.isRequired, 85 | openChangePasswordDialog: PropTypes.func.isRequired, 86 | }; 87 | 88 | export default DialogSelector; 89 | -------------------------------------------------------------------------------- /src/logged_out/components/register_login/LoginDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useRef, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { withRouter } from "react-router-dom"; 5 | import { TextField, Button, Checkbox, Typography, FormControlLabel } from "@mui/material"; 6 | import withStyles from '@mui/styles/withStyles'; 7 | import FormDialog from "../../../shared/components/FormDialog"; 8 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 9 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 10 | import VisibilityPasswordTextField from "../../../shared/components/VisibilityPasswordTextField"; 11 | 12 | const styles = (theme) => ({ 13 | forgotPassword: { 14 | marginTop: theme.spacing(2), 15 | color: theme.palette.primary.main, 16 | cursor: "pointer", 17 | "&:enabled:hover": { 18 | color: theme.palette.primary.dark, 19 | }, 20 | "&:enabled:focus": { 21 | color: theme.palette.primary.dark, 22 | }, 23 | }, 24 | disabledText: { 25 | cursor: "auto", 26 | color: theme.palette.text.disabled, 27 | }, 28 | formControlLabel: { 29 | marginRight: 0, 30 | }, 31 | }); 32 | 33 | function LoginDialog(props) { 34 | const { 35 | setStatus, 36 | history, 37 | classes, 38 | onClose, 39 | openChangePasswordDialog, 40 | status, 41 | } = props; 42 | const [isLoading, setIsLoading] = useState(false); 43 | const [isPasswordVisible, setIsPasswordVisible] = useState(false); 44 | const loginEmail = useRef(); 45 | const loginPassword = useRef(); 46 | 47 | const login = useCallback(() => { 48 | setIsLoading(true); 49 | setStatus(null); 50 | if (loginEmail.current.value !== "test@web.com") { 51 | setTimeout(() => { 52 | setStatus("invalidEmail"); 53 | setIsLoading(false); 54 | }, 1500); 55 | } else if (loginPassword.current.value !== "HaRzwc") { 56 | setTimeout(() => { 57 | setStatus("invalidPassword"); 58 | setIsLoading(false); 59 | }, 1500); 60 | } else { 61 | setTimeout(() => { 62 | history.push("/c/dashboard"); 63 | }, 150); 64 | } 65 | }, [setIsLoading, loginEmail, loginPassword, history, setStatus]); 66 | 67 | return ( 68 | 69 | { 74 | e.preventDefault(); 75 | login(); 76 | }} 77 | hideBackdrop 78 | headline="Login" 79 | content={ 80 | 81 | { 93 | if (status === "invalidEmail") { 94 | setStatus(null); 95 | } 96 | }} 97 | helperText={ 98 | status === "invalidEmail" && 99 | "This email address isn't associated with an account." 100 | } 101 | FormHelperTextProps={{ error: true }} 102 | /> 103 | { 113 | if (status === "invalidPassword") { 114 | setStatus(null); 115 | } 116 | }} 117 | helperText={ 118 | status === "invalidPassword" ? ( 119 | 120 | Incorrect password. Try again, or click on{" "} 121 | "Forgot Password?" to reset it. 122 | 123 | ) : ( 124 | "" 125 | ) 126 | } 127 | FormHelperTextProps={{ error: true }} 128 | onVisibilityChange={setIsPasswordVisible} 129 | isVisible={isPasswordVisible} 130 | /> 131 | } 134 | label={Remember me} 135 | /> 136 | {status === "verificationEmailSend" ? ( 137 | 138 | We have send instructions on how to reset your password to your 139 | email address 140 | 141 | ) : ( 142 | 143 | Email is: test@web.com 144 |
145 | Password is: HaRzwc 146 |
147 | )} 148 |
149 | } 150 | actions={ 151 | 152 | 163 | { 174 | // For screenreaders listen to space and enter events 175 | if ( 176 | (!isLoading && event.keyCode === 13) || 177 | event.keyCode === 32 178 | ) { 179 | openChangePasswordDialog(); 180 | } 181 | }} 182 | > 183 | Forgot Password? 184 | 185 | 186 | } 187 | /> 188 |
189 | ); 190 | } 191 | 192 | LoginDialog.propTypes = { 193 | classes: PropTypes.object.isRequired, 194 | onClose: PropTypes.func.isRequired, 195 | setStatus: PropTypes.func.isRequired, 196 | openChangePasswordDialog: PropTypes.func.isRequired, 197 | history: PropTypes.object.isRequired, 198 | status: PropTypes.string, 199 | }; 200 | 201 | export default withRouter(withStyles(styles)(LoginDialog)); 202 | -------------------------------------------------------------------------------- /src/logged_out/dummy_data/blogPosts.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Typography } from "@mui/material"; 3 | 4 | const content = ( 5 | 6 | 7 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 8 | eirmod tempor invidunt ut labore. 9 | 10 | 11 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 12 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 13 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 14 | clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 15 | amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam 16 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed 17 | diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. 18 | Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor 19 | sit amet. 20 | 21 | 22 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 23 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 24 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 25 | clita kasd gubergren, no sea takimata sanctus est Lorem. 26 | 27 | 28 | Title 29 | 30 | 31 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 32 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 33 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 34 | clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 35 | amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam 36 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed 37 | diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. 38 | Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor 39 | sit amet. 40 | 41 | 42 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 43 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 44 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 45 | clita kasd gubergren, no sea takimata sanctus est Lorem. 46 | 47 | 48 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 49 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 50 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 51 | clita kasd gubergren, no sea takimata sanctus est Lorem. 52 | 53 | 54 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 55 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 56 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 57 | clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 58 | amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam 59 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed 60 | diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. 61 | Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor 62 | sit amet. 63 | 64 | 65 | Title 66 | 67 | 68 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 69 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 70 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 71 | clita kasd gubergren, no sea takimata sanctus est Lorem. 72 | 73 | 74 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 75 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 76 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet 77 | clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 78 | amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam 79 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed 80 | diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. 81 | Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor 82 | sit amet. 83 | 84 | 85 | ); 86 | 87 | const posts = [ 88 | { 89 | title: "Post 1", 90 | id: 1, 91 | date: 1576281600, 92 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost1.jpg`, 93 | snippet: 94 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 95 | content: content, 96 | }, 97 | { 98 | title: "Post 2", 99 | id: 2, 100 | date: 1576391600, 101 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost2.jpg`, 102 | snippet: 103 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 104 | content: content, 105 | }, 106 | { 107 | title: "Post 3", 108 | id: 3, 109 | date: 1577391600, 110 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost3.jpg`, 111 | snippet: 112 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 113 | content: content, 114 | }, 115 | { 116 | title: "Post 4", 117 | id: 4, 118 | date: 1572281600, 119 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost4.jpg`, 120 | snippet: 121 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 122 | content: content, 123 | }, 124 | { 125 | title: "Post 5", 126 | id: 5, 127 | date: 1573281600, 128 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost5.jpg`, 129 | snippet: 130 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 131 | content: content, 132 | }, 133 | { 134 | title: "Post 6", 135 | id: 6, 136 | date: 1575281600, 137 | src: `${process.env.PUBLIC_URL}/images/logged_out/blogPost6.jpg`, 138 | snippet: 139 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", 140 | content: content, 141 | }, 142 | ]; 143 | 144 | export default posts; -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from 'workbox-core'; 11 | import { ExpirationPlugin } from 'workbox-expiration'; 12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 13 | import { registerRoute } from 'workbox-routing'; 14 | import { StaleWhileRevalidate } from 'workbox-strategies'; 15 | 16 | clientsClaim(); 17 | 18 | // Precache all of the assets generated by your build process. 19 | // Their URLs are injected into the manifest variable below. 20 | // This variable must be present somewhere in your service worker file, 21 | // even if you decide not to use precaching. See https://cra.link/PWA 22 | precacheAndRoute(self.__WB_MANIFEST); 23 | 24 | // Set up App Shell-style routing, so that all navigation requests 25 | // are fulfilled with your index.html shell. Learn more at 26 | // https://developers.google.com/web/fundamentals/architecture/app-shell 27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 28 | registerRoute( 29 | // Return false to exempt requests from being fulfilled by index.html. 30 | ({ request, url }) => { 31 | // If this isn't a navigation, skip. 32 | if (request.mode !== 'navigate') { 33 | return false; 34 | } // If this is a URL that starts with /_, skip. 35 | 36 | if (url.pathname.startsWith('/_')) { 37 | return false; 38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 39 | 40 | if (url.pathname.match(fileExtensionRegexp)) { 41 | return false; 42 | } // Return true to signal that we want to use the handler. 43 | 44 | return true; 45 | }, 46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 47 | ); 48 | 49 | // An example runtime caching route for requests that aren't handled by the 50 | // precache, in this case same-origin .png requests like those from in public/ 51 | registerRoute( 52 | // Add in any other file extensions or routing criteria as needed. 53 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. 54 | new StaleWhileRevalidate({ 55 | cacheName: 'images', 56 | plugins: [ 57 | // Ensure that once this runtime cache reaches a maximum size the 58 | // least-recently used images are removed. 59 | new ExpirationPlugin({ maxEntries: 50 }), 60 | ], 61 | }) 62 | ); 63 | 64 | // This allows the web app to trigger skipWaiting via 65 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 66 | self.addEventListener('message', (event) => { 67 | if (event.data && event.data.type === 'SKIP_WAITING') { 68 | self.skipWaiting(); 69 | } 70 | }); 71 | 72 | // Any other custom service worker logic can go here. 73 | -------------------------------------------------------------------------------- /src/serviceWorkerRegistration.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://cra.link/PWA' 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then((registration) => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | 'New content is available and will be used when all ' + 72 | 'tabs for this page are closed. See https://cra.link/PWA.' 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log('Content is cached for offline use.'); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch((error) => { 95 | console.error('Error during service worker registration:', error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl, { 102 | headers: { 'Service-Worker': 'script' }, 103 | }) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log('No internet connection found. App is running in offline mode.'); 124 | }); 125 | } 126 | 127 | export function unregister() { 128 | if ('serviceWorker' in navigator) { 129 | navigator.serviceWorker.ready 130 | .then((registration) => { 131 | registration.unregister(); 132 | }) 133 | .catch((error) => { 134 | console.error(error.message); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/shared/components/ActionPaper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Paper, DialogTitle, DialogContent, DialogActions, Box } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = theme => ({ 8 | helpPadding: { 9 | "@media (max-width: 400px)": { 10 | paddingLeft: theme.spacing(1), 11 | paddingRight: theme.spacing(1) 12 | } 13 | }, 14 | fullWidth: { 15 | width: "100%" 16 | } 17 | }); 18 | 19 | function ActionPaper(props) { 20 | const { 21 | theme, 22 | classes, 23 | title, 24 | content, 25 | maxWidth, 26 | actions, 27 | helpPadding, 28 | fullWidthActions 29 | } = props; 30 | return ( 31 | 32 | 33 | {title && {title}} 34 | {content && ( 35 | 38 | {content} 39 | 40 | )} 41 | {actions && ( 42 | 43 | 46 | {actions} 47 | 48 | 49 | )} 50 | 51 | 52 | ); 53 | } 54 | 55 | ActionPaper.propTypes = { 56 | theme: PropTypes.object.isRequired, 57 | classes: PropTypes.object.isRequired, 58 | title: PropTypes.oneOfType([ 59 | PropTypes.element, 60 | PropTypes.func, 61 | PropTypes.string 62 | ]), 63 | content: PropTypes.element, 64 | maxWidth: PropTypes.string, 65 | actions: PropTypes.element, 66 | helpPadding: PropTypes.bool, 67 | fullWidthActions: PropTypes.bool 68 | }; 69 | 70 | export default withStyles(styles, { withTheme: true })(ActionPaper); 71 | -------------------------------------------------------------------------------- /src/shared/components/Bordered.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from '@mui/styles/withStyles'; 4 | 5 | const styles = theme => ({ 6 | wrapper: { 7 | border: `${theme.border.borderWidth}px solid ${theme.border.borderColor}` 8 | }, 9 | greyed: { 10 | border: `${theme.border.borderWidth}px solid rgba(0, 0, 0, 0.23)` 11 | } 12 | }); 13 | 14 | function Bordered(props) { 15 | const { 16 | classes, 17 | theme, 18 | disableVerticalPadding, 19 | disableBorderRadius, 20 | children, 21 | variant 22 | } = props; 23 | return ( 24 |
32 | {children} 33 |
34 | ); 35 | } 36 | 37 | Bordered.propTypes = { 38 | classes: PropTypes.object, 39 | theme: PropTypes.object, 40 | disableVerticalPadding: PropTypes.bool, 41 | disableBorderRadius: PropTypes.bool, 42 | children: PropTypes.oneOfType([ 43 | PropTypes.element, 44 | PropTypes.func, 45 | PropTypes.array 46 | ]), 47 | variant: PropTypes.string 48 | }; 49 | 50 | export default withStyles(styles, { withTheme: true })(Bordered); 51 | -------------------------------------------------------------------------------- /src/shared/components/ButtonCircularProgress.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { CircularProgress, Box } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = theme => ({ 8 | circularProgress: { 9 | color: theme.palette.secondary.main 10 | } 11 | }); 12 | 13 | function ButtonCircularProgress(props) { 14 | const { size, classes } = props; 15 | return ( 16 | 17 | 22 | 23 | ); 24 | } 25 | 26 | ButtonCircularProgress.propTypes = { 27 | size: PropTypes.number, 28 | classes: PropTypes.object.isRequired 29 | }; 30 | 31 | export default withStyles(styles, { withTheme: true })(ButtonCircularProgress); 32 | -------------------------------------------------------------------------------- /src/shared/components/CardChart.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | AreaChart, 5 | Area, 6 | XAxis, 7 | Tooltip, 8 | ResponsiveContainer, 9 | YAxis, 10 | } from "recharts"; 11 | import format from "date-fns/format"; 12 | import { Card, CardContent, Typography, IconButton, Menu, MenuItem, Box } from "@mui/material"; 13 | import withStyles from '@mui/styles/withStyles'; 14 | import MoreVertIcon from "@mui/icons-material/MoreVert"; 15 | 16 | const styles = (theme) => ({ 17 | cardContentInner: { 18 | marginTop: theme.spacing(-4), 19 | }, 20 | }); 21 | 22 | function labelFormatter(label) { 23 | return format(new Date(label * 1000), "MMMM d, p yyyy"); 24 | } 25 | 26 | function calculateMin(data, yKey, factor) { 27 | let max = Number.POSITIVE_INFINITY; 28 | data.forEach((element) => { 29 | if (max > element[yKey]) { 30 | max = element[yKey]; 31 | } 32 | }); 33 | return Math.round(max - max * factor); 34 | } 35 | 36 | const itemHeight = 216; 37 | const options = ["1 Week", "1 Month", "6 Months"]; 38 | 39 | function CardChart(props) { 40 | const { color, data, title, classes, theme, height } = props; 41 | const [anchorEl, setAnchorEl] = useState(null); 42 | const [selectedOption, setSelectedOption] = useState("1 Month"); 43 | 44 | const handleClick = useCallback( 45 | (event) => { 46 | setAnchorEl(event.currentTarget); 47 | }, 48 | [setAnchorEl] 49 | ); 50 | 51 | const formatter = useCallback( 52 | (value) => { 53 | return [value, title]; 54 | }, 55 | [title] 56 | ); 57 | 58 | const getSubtitle = useCallback(() => { 59 | switch (selectedOption) { 60 | case "1 Week": 61 | return "Last week"; 62 | case "1 Month": 63 | return "Last month"; 64 | case "6 Months": 65 | return "Last 6 months"; 66 | default: 67 | throw new Error("No branch selected in switch-statement"); 68 | } 69 | }, [selectedOption]); 70 | 71 | const processData = useCallback(() => { 72 | let seconds; 73 | switch (selectedOption) { 74 | case "1 Week": 75 | seconds = 60 * 60 * 24 * 7; 76 | break; 77 | case "1 Month": 78 | seconds = 60 * 60 * 24 * 31; 79 | break; 80 | case "6 Months": 81 | seconds = 60 * 60 * 24 * 31 * 6; 82 | break; 83 | default: 84 | throw new Error("No branch selected in switch-statement"); 85 | } 86 | const minSeconds = new Date() / 1000 - seconds; 87 | const arr = []; 88 | for (let i = 0; i < data.length; i += 1) { 89 | if (minSeconds < data[i].timestamp) { 90 | arr.unshift(data[i]); 91 | } 92 | } 93 | return arr; 94 | }, [data, selectedOption]); 95 | 96 | const handleClose = useCallback(() => { 97 | setAnchorEl(null); 98 | }, [setAnchorEl]); 99 | 100 | const selectOption = useCallback( 101 | (selectedOption) => { 102 | setSelectedOption(selectedOption); 103 | handleClose(); 104 | }, 105 | [setSelectedOption, handleClose] 106 | ); 107 | 108 | const isOpen = Boolean(anchorEl); 109 | return ( 110 | 111 | 112 | 113 |
114 | {title} 115 | 116 | {getSubtitle()} 117 | 118 |
119 |
120 | 126 | 127 | 128 | 141 | {options.map((option) => ( 142 | { 146 | selectOption(option); 147 | }} 148 | name={option} 149 | > 150 | {option} 151 | 152 | ))} 153 | 154 |
155 |
156 |
157 | 158 | 159 | 160 | 161 | 167 | 171 | 177 | 196 | 197 | 198 | 199 | 200 |
201 | ); 202 | } 203 | 204 | CardChart.propTypes = { 205 | color: PropTypes.string.isRequired, 206 | data: PropTypes.array.isRequired, 207 | title: PropTypes.string.isRequired, 208 | classes: PropTypes.object.isRequired, 209 | theme: PropTypes.object.isRequired, 210 | height: PropTypes.string.isRequired, 211 | }; 212 | 213 | export default withStyles(styles, { withTheme: true })(CardChart); 214 | -------------------------------------------------------------------------------- /src/shared/components/ColoredButton.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button, createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from "@mui/material"; 4 | 5 | function ColoredButton(props) { 6 | const { color, children, theme } = props; 7 | const buttonTheme = createTheme(adaptV4Theme({ 8 | ...theme, 9 | palette: { 10 | primary: { 11 | main: color 12 | } 13 | } 14 | })); 15 | const buttonProps = (({ color, theme, children, ...o }) => o)(props); 16 | return ( 17 | 18 | 19 | 22 | 23 | 24 | ); 25 | } 26 | 27 | ColoredButton.propTypes = { 28 | color: PropTypes.string.isRequired 29 | }; 30 | 31 | export default memo(ColoredButton); 32 | -------------------------------------------------------------------------------- /src/shared/components/ColorfulChip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Chip } from "@mui/material"; 4 | import shadeColor from "../functions/shadeColor"; 5 | 6 | function ColorfulChip(props) { 7 | const { color, label, className } = props; 8 | return ( 9 | 17 | ); 18 | } 19 | 20 | ColorfulChip.propTypes = { 21 | color: PropTypes.string.isRequired, 22 | label: PropTypes.string.isRequired, 23 | className: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) 24 | }; 25 | 26 | export default ColorfulChip; 27 | -------------------------------------------------------------------------------- /src/shared/components/ConfirmationDialog.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Dialog, 5 | DialogTitle, 6 | DialogContent, 7 | DialogContentText, 8 | DialogActions, 9 | Button 10 | } from "@mui/material"; 11 | import ButtonCircularProgress from "./ButtonCircularProgress"; 12 | 13 | function ConfirmationDialog(props) { 14 | const { open, onClose, loading, title, content, onConfirm } = props; 15 | return ( 16 | 17 | {title} 18 | 19 | {content} 20 | 21 | 22 | 25 | 33 | 34 | 35 | ); 36 | } 37 | 38 | ConfirmationDialog.propTypes = { 39 | open: PropTypes.bool, 40 | onClose: PropTypes.func, 41 | loading: PropTypes.bool, 42 | title: PropTypes.string, 43 | content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 44 | onConfirm: PropTypes.func 45 | }; 46 | 47 | export default ConfirmationDialog; 48 | -------------------------------------------------------------------------------- /src/shared/components/ConsecutiveSnackbarMessages.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useRef, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Snackbar } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = (theme) => ({ 8 | root: { 9 | backgroundColor: theme.palette.primary.main, 10 | paddingTop: 0, 11 | paddingBottom: 0, 12 | }, 13 | }); 14 | 15 | function ConsecutiveSnackbars(props) { 16 | const { classes, getPushMessageFromChild } = props; 17 | const [isOpen, setIsOpen] = useState(false); 18 | const [messageInfo, setMessageInfo] = useState({}); 19 | const queue = useRef([]); 20 | 21 | const processQueue = useCallback(() => { 22 | if (queue.current.length > 0) { 23 | setMessageInfo(queue.current.shift()); 24 | setIsOpen(true); 25 | } 26 | }, [setMessageInfo, setIsOpen, queue]); 27 | 28 | const handleClose = useCallback((_, reason) => { 29 | if (reason === "clickaway") { 30 | return; 31 | } 32 | setIsOpen(false); 33 | }, [setIsOpen]); 34 | 35 | const pushMessage = useCallback(message => { 36 | queue.current.push({ 37 | message, 38 | key: new Date().getTime(), 39 | }); 40 | if (isOpen) { 41 | // immediately begin dismissing current message 42 | // to start showing new one 43 | setIsOpen(false); 44 | } else { 45 | processQueue(); 46 | } 47 | }, [queue, isOpen, setIsOpen, processQueue]); 48 | 49 | useEffect(() => { 50 | getPushMessageFromChild(pushMessage); 51 | }, [getPushMessageFromChild, pushMessage]); 52 | 53 | return ( 54 | {messageInfo.message ? messageInfo.message.text : null} 71 | } 72 | TransitionProps={{ 73 | onExited: processQueue 74 | }} /> 75 | ); 76 | 77 | } 78 | 79 | ConsecutiveSnackbars.propTypes = { 80 | getPushMessageFromChild: PropTypes.func.isRequired, 81 | classes: PropTypes.object.isRequired, 82 | }; 83 | 84 | export default withStyles(styles, { withTheme: true })(ConsecutiveSnackbars); 85 | -------------------------------------------------------------------------------- /src/shared/components/DateTimePicker.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { TextField } from "@mui/material"; 4 | import MobileDatePicker from "@mui/lab/MobileDatePicker"; 5 | import AdapterDateFns from "@mui/lab/AdapterDateFns"; 6 | import LocalizationProvider from "@mui/lab/LocalizationProvider"; 7 | import { 8 | ThemeProvider, 9 | StyledEngineProvider, 10 | createTheme, 11 | adaptV4Theme, 12 | } from "@mui/material"; 13 | import withTheme from "@mui/styles/withTheme"; 14 | import AccessTime from "@mui/icons-material/AccessTime"; 15 | import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; 16 | import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; 17 | import DateRange from "@mui/icons-material/DateRange"; 18 | 19 | const Theme2 = (theme) => 20 | createTheme( 21 | adaptV4Theme({ 22 | ...theme, 23 | overrides: { 24 | MuiOutlinedInput: { 25 | root: { 26 | width: 190, 27 | "@media (max-width: 400px)": { 28 | width: 160, 29 | }, 30 | "@media (max-width: 360px)": { 31 | width: 140, 32 | }, 33 | "@media (max-width: 340px)": { 34 | width: 120, 35 | }, 36 | }, 37 | input: { 38 | padding: "9px 14.5px", 39 | }, 40 | }, 41 | }, 42 | }) 43 | ); 44 | 45 | function DTPicker(props) { 46 | const { disabled, value, onChange } = props; 47 | return ( 48 | 49 | 50 | 51 | } 54 | rightArrowIcon={} 55 | timeIcon={} 56 | dateRangeIcon={} 57 | variant="outlined" 58 | disabled={disabled} 59 | value={value} 60 | onChange={onChange} 61 | renderInput={(params) => } 62 | {...props} 63 | /> 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | DTPicker.propTypes = { 71 | disabled: PropTypes.bool, 72 | value: PropTypes.instanceOf(Date), 73 | onChange: PropTypes.func, 74 | }; 75 | 76 | export default withTheme(DTPicker); 77 | -------------------------------------------------------------------------------- /src/shared/components/DialogTitleWithCloseIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { IconButton, DialogTitle, Typography, Box } from "@mui/material"; 4 | import withTheme from '@mui/styles/withTheme'; 5 | import CloseIcon from "@mui/icons-material/Close"; 6 | 7 | function DialogTitleWithCloseIcon(props) { 8 | const { 9 | theme, 10 | paddingBottom, 11 | onClose, 12 | disabled, 13 | title, 14 | disablePadding 15 | } = props; 16 | return ( 17 | 29 | 30 | {title} 31 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | DialogTitleWithCloseIcon.propTypes = { 45 | theme: PropTypes.object, 46 | paddingBottom: PropTypes.number, 47 | onClose: PropTypes.func, 48 | disabled: PropTypes.bool, 49 | title: PropTypes.string, 50 | disablePadding: PropTypes.bool 51 | }; 52 | 53 | export default withTheme(DialogTitleWithCloseIcon); 54 | -------------------------------------------------------------------------------- /src/shared/components/Dropzone.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDropzone } from "react-dropzone"; 4 | import classNames from "classnames"; 5 | import { Box } from "@mui/material"; 6 | import withStyles from '@mui/styles/withStyles'; 7 | import ColoredButton from "./ColoredButton"; 8 | 9 | const styles = { 10 | button: { 11 | borderWidth: 1, 12 | borderColor: "rgba(0, 0, 0, 0.23)", 13 | borderTopLeftRadius: 0, 14 | borderBottomLeftRadius: 0 15 | }, 16 | fullHeight: { 17 | height: "100%" 18 | } 19 | }; 20 | 21 | function getColor(isDragAccept, isDragReject, theme) { 22 | if (isDragAccept) { 23 | return theme.palette.success.main; 24 | } 25 | if (isDragReject) { 26 | return theme.palette.error.dark; 27 | } 28 | return theme.palette.common.black; 29 | } 30 | 31 | function Dropzone(props) { 32 | const { onDrop, accept, fullHeight, children, classes, style, theme } = props; 33 | const { 34 | getRootProps, 35 | getInputProps, 36 | isDragAccept, 37 | isDragReject 38 | } = useDropzone({ 39 | accept: accept, 40 | onDrop: onDrop 41 | }); 42 | return ( 43 | 44 | 45 | 55 | {children} 56 | 57 | 58 | ); 59 | } 60 | 61 | Dropzone.propTypes = { 62 | classes: PropTypes.object.isRequired, 63 | theme: PropTypes.object.isRequired, 64 | onDrop: PropTypes.func, 65 | accept: PropTypes.string, 66 | fullHeight: PropTypes.bool, 67 | style: PropTypes.object, 68 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) 69 | }; 70 | 71 | export default withStyles(styles, { withTheme: true })(Dropzone); 72 | -------------------------------------------------------------------------------- /src/shared/components/EmojiTextArea.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import "emoji-mart/css/emoji-mart.css"; 4 | import { Picker } from "emoji-mart"; 5 | import { TextField, IconButton, Collapse, FormHelperText, Box, Grid } from "@mui/material"; 6 | import withStyles from '@mui/styles/withStyles'; 7 | import EmojiEmotionsIcon from "@mui/icons-material/EmojiEmotions"; 8 | import CloseIcon from "@mui/icons-material/Close"; 9 | import countWithEmojis from "../functions/countWithEmojis"; 10 | 11 | const styles = theme => ({ 12 | "@global": { 13 | ".emoji-mart-category-label": theme.typography.body1, 14 | ".emoji-mart-bar": { display: "none !important" }, 15 | ".emoji-mart-search input": { 16 | ...theme.typography.body1, 17 | ...theme.border 18 | }, 19 | ".emoji-mart-search": { 20 | marginTop: `${theme.spacing(1)} !important`, 21 | paddingRight: `${theme.spacing(1)} !important`, 22 | paddingLeft: `${theme.spacing(1)} !important`, 23 | paddingBottom: `${theme.spacing(1)} !important` 24 | }, 25 | ".emoji-mart-search-icon": { 26 | top: "5px !important", 27 | right: "14px !important", 28 | fontSize: 20 29 | }, 30 | ".emoji-mart-scroll": { 31 | height: 240 32 | }, 33 | ".emoji-mart": { 34 | ...theme.border 35 | } 36 | }, 37 | floatButtonWrapper: { 38 | position: "absolute", 39 | bottom: 12, 40 | right: 12 41 | }, 42 | floatButtonSVG: { 43 | color: theme.palette.primary.light 44 | }, 45 | relative: { 46 | position: "relative" 47 | } 48 | }); 49 | 50 | /** 51 | * Emojis whose unified is greater than 5 sometimes 52 | * are not displayed correcty in the browser. 53 | * We won't display them. 54 | */ 55 | const emojisToShowFilter = emoji => { 56 | if (emoji.unified.length > 5) { 57 | return false; 58 | } 59 | return true; 60 | }; 61 | 62 | function EmojiTextarea(props) { 63 | const { 64 | theme, 65 | classes, 66 | rightContent, 67 | placeholder, 68 | maxCharacters, 69 | emojiSet, 70 | inputClassName, 71 | onChange 72 | } = props; 73 | const [open, setOpen] = useState(false); 74 | const [value, setValue] = useState(""); 75 | const [characters, setCharacters] = useState(0); 76 | 77 | const onSelectEmoji = useCallback( 78 | emoji => { 79 | let _characters; 80 | let _value = value + emoji.native; 81 | if (maxCharacters) { 82 | _characters = countWithEmojis(_value); 83 | if (_characters > maxCharacters) { 84 | return; 85 | } 86 | } 87 | if (onChange) { 88 | onChange(_value, _characters); 89 | } 90 | setValue(_value); 91 | setCharacters(_characters); 92 | }, 93 | [value, setValue, setCharacters, maxCharacters, onChange] 94 | ); 95 | 96 | const handleTextFieldChange = useCallback( 97 | event => { 98 | const { target } = event; 99 | const { value } = target; 100 | let characters; 101 | if (maxCharacters) { 102 | characters = countWithEmojis(value); 103 | if (characters > maxCharacters) { 104 | return; 105 | } 106 | } 107 | if (onChange) { 108 | onChange(value, characters); 109 | } 110 | setValue(value); 111 | setCharacters(characters); 112 | }, 113 | [maxCharacters, onChange, setValue, setCharacters] 114 | ); 115 | 116 | const toggleOpen = useCallback(() => { 117 | setOpen(!open); 118 | }, [open, setOpen]); 119 | 120 | return ( 121 | 122 | 123 | 130 | 144 |
145 | 146 | {open ? ( 147 | 148 | ) : ( 149 | 150 | )} 151 | 152 |
153 |
154 | {rightContent && ( 155 | 156 | {rightContent} 157 | 158 | )} 159 |
160 | {maxCharacters && ( 161 | = maxCharacters}> 162 | {`${characters}/${maxCharacters} characters`} 163 | 164 | )} 165 | 166 | 167 | 174 | 175 | 176 |
177 | ); 178 | } 179 | 180 | EmojiTextarea.propTypes = { 181 | theme: PropTypes.object.isRequired, 182 | classes: PropTypes.object.isRequired, 183 | emojiSet: PropTypes.string.isRequired, 184 | rightContent: PropTypes.element, 185 | placeholder: PropTypes.string, 186 | maxCharacters: PropTypes.number, 187 | onChange: PropTypes.func, 188 | inputClassName: PropTypes.string 189 | }; 190 | 191 | export default withStyles(styles, { withTheme: true })(EmojiTextarea); 192 | -------------------------------------------------------------------------------- /src/shared/components/EnhancedTableHead.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { Typography, TableCell, TableHead, TableRow, TableSortLabel, Tooltip } from "@mui/material"; 5 | 6 | import withStyles from '@mui/styles/withStyles'; 7 | 8 | const styles = theme => ({ 9 | tableSortLabel: { 10 | cursor: "text", 11 | userSelect: "auto", 12 | color: "inherit !important" 13 | }, 14 | noIcon: { 15 | "& path": { 16 | display: "none !important" 17 | } 18 | }, 19 | paddingFix: { 20 | paddingLeft: theme.spacing(3) 21 | } 22 | }); 23 | 24 | function EnhancedTableHead(props) { 25 | const { order, orderBy, rows, onRequestSort, classes } = props; 26 | 27 | const createSortHandler = useCallback( 28 | property => event => { 29 | onRequestSort(event, property); 30 | }, 31 | [onRequestSort] 32 | ); 33 | 34 | return ( 35 | 36 | 37 | {rows.map((row, index) => ( 38 | 45 | {onRequestSort ? ( 46 | 51 | 56 | {row.label} 57 | 58 | 59 | ) : ( 60 | 63 | 64 | {row.label} 65 | 66 | 67 | )} 68 | 69 | ))} 70 | 71 | 72 | ); 73 | } 74 | EnhancedTableHead.propTypes = { 75 | classes: PropTypes.object.isRequired, 76 | theme: PropTypes.object.isRequired, 77 | onRequestSort: PropTypes.func, 78 | order: PropTypes.string, 79 | orderBy: PropTypes.string, 80 | rows: PropTypes.arrayOf(PropTypes.object).isRequired 81 | }; 82 | 83 | export default withStyles(styles, { withTheme: true })(EnhancedTableHead); 84 | -------------------------------------------------------------------------------- /src/shared/components/FormDialog.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Dialog, DialogContent, Box } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import DialogTitleWithCloseIcon from "./DialogTitleWithCloseIcon"; 6 | 7 | const styles = theme => ({ 8 | dialogPaper: { 9 | display: "flex", 10 | flexDirection: "column", 11 | alignItems: "center", 12 | paddingBottom: theme.spacing(3), 13 | maxWidth: 420 14 | }, 15 | actions: { 16 | marginTop: theme.spacing(2) 17 | }, 18 | dialogPaperScrollPaper: { 19 | maxHeight: "none" 20 | }, 21 | dialogContent: { 22 | paddingTop: 0, 23 | paddingBottom: 0 24 | } 25 | }); 26 | 27 | /** 28 | * A Wrapper around the Dialog component to create centered 29 | * Login, Register or other Dialogs. 30 | */ 31 | function FormDialog(props) { 32 | const { 33 | classes, 34 | open, 35 | onClose, 36 | loading, 37 | headline, 38 | onFormSubmit, 39 | content, 40 | actions, 41 | hideBackdrop 42 | } = props; 43 | return ( 44 | 53 | 58 | 59 |
60 |
{content}
61 | 62 | {actions} 63 | 64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | FormDialog.propTypes = { 71 | classes: PropTypes.object.isRequired, 72 | open: PropTypes.bool.isRequired, 73 | onClose: PropTypes.func.isRequired, 74 | headline: PropTypes.string.isRequired, 75 | loading: PropTypes.bool.isRequired, 76 | onFormSubmit: PropTypes.func.isRequired, 77 | content: PropTypes.element.isRequired, 78 | actions: PropTypes.element.isRequired, 79 | hideBackdrop: PropTypes.bool.isRequired 80 | }; 81 | 82 | export default withStyles(styles, { withTheme: true })(FormDialog); 83 | -------------------------------------------------------------------------------- /src/shared/components/HelpIcon.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Tooltip, Typography } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import HelpIconOutline from "@mui/icons-material/HelpOutline"; 6 | 7 | const styles = theme => ({ 8 | tooltipTypo: { 9 | whiteSpace: "pre-line !important", 10 | ...theme.typography.caption, 11 | color: theme.palette.common.white 12 | }, 13 | tooltip: { 14 | verticalAlign: "middle", 15 | fontSize: "1.25rem" 16 | }, 17 | helpIcon: { 18 | marginLeft: theme.spacing(1), 19 | "@media (max-width: 350px)": { 20 | marginLeft: theme.spacing(0.5) 21 | }, 22 | transition: theme.transitions.create(["color"], { 23 | duration: theme.transitions.duration.short, 24 | easing: theme.transitions.easing.easeInOut 25 | }) 26 | } 27 | }); 28 | 29 | function HelpIcon(props) { 30 | const { classes, title } = props; 31 | const [isHovered, setIsHovered] = useState(false); 32 | 33 | const onMouseOver = useCallback(() => { 34 | setIsHovered(true); 35 | }, []); 36 | 37 | const onMouseLeave = useCallback(() => { 38 | setIsHovered(false); 39 | }, []); 40 | 41 | return ( 42 | 45 | {title} 46 | 47 | } 48 | className={classes.tooltip} 49 | enterTouchDelay={300} 50 | > 51 | 64 | 65 | ); 66 | } 67 | 68 | HelpIcon.propTypes = { 69 | classes: PropTypes.object, 70 | title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) 71 | }; 72 | 73 | export default withStyles(styles, { withTheme: true })(HelpIcon); 74 | -------------------------------------------------------------------------------- /src/shared/components/HighlightedInformation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { Typography } from "@mui/material"; 5 | 6 | import withStyles from '@mui/styles/withStyles'; 7 | 8 | const styles = theme => ({ 9 | main: { 10 | backgroundColor: theme.palette.warning.light, 11 | border: `${theme.border.borderWidth}px solid ${theme.palette.warning.main}`, 12 | padding: theme.spacing(2), 13 | borderRadius: theme.shape.borderRadius 14 | } 15 | }); 16 | 17 | function HighlighedInformation(props) { 18 | const { className, children, classes } = props; 19 | return ( 20 |
21 | {children} 22 |
23 | ); 24 | } 25 | 26 | HighlighedInformation.propTypes = { 27 | classes: PropTypes.object.isRequired, 28 | children: PropTypes.oneOfType([ 29 | PropTypes.string, 30 | PropTypes.element, 31 | PropTypes.array 32 | ]).isRequired, 33 | className: PropTypes.string 34 | }; 35 | 36 | export default withStyles(styles, { withTheme: true })(HighlighedInformation); 37 | -------------------------------------------------------------------------------- /src/shared/components/ImageCropperDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Dialog, DialogContent, DialogActions, Button, Box } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = (theme) => ({ 8 | dialogPaper: { maxWidth: `${theme.breakpoints.values.md}px !important` }, 9 | dialogContent: { 10 | paddingTop: theme.spacing(2), 11 | paddingRight: theme.spacing(2), 12 | paddingLeft: theme.spacing(2), 13 | }, 14 | }); 15 | 16 | function ImageCropperDialog(props) { 17 | const { 18 | ImageCropper, 19 | classes, 20 | onClose, 21 | open, 22 | src, 23 | onCrop, 24 | aspectRatio, 25 | theme, 26 | } = props; 27 | const [crop, setCrop] = useState(null); 28 | 29 | const getCropFunctionFromChild = useCallback( 30 | (cropFunction) => { 31 | setCrop(() => cropFunction); 32 | }, 33 | [setCrop] 34 | ); 35 | 36 | return ( 37 | 43 | 44 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | ); 62 | } 63 | 64 | ImageCropperDialog.propTypes = { 65 | ImageCropper: PropTypes.elementType, 66 | classes: PropTypes.object.isRequired, 67 | onClose: PropTypes.func.isRequired, 68 | open: PropTypes.bool.isRequired, 69 | onCrop: PropTypes.func.isRequired, 70 | src: PropTypes.string, 71 | aspectRatio: PropTypes.number, 72 | }; 73 | 74 | export default withStyles(styles, { withTheme: true })(ImageCropperDialog); 75 | -------------------------------------------------------------------------------- /src/shared/components/ModalBackdrop.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Backdrop } from "@mui/material"; 4 | 5 | import withStyles from '@mui/styles/withStyles'; 6 | 7 | const styles = { 8 | backdrop: { 9 | top: 0, 10 | left: 0, 11 | right: 0, 12 | bottom: 0, 13 | zIndex: 1200, 14 | position: "fixed", 15 | touchAction: "none", 16 | backgroundColor: "rgba(0, 0, 0, 0.5)" 17 | } 18 | }; 19 | 20 | function ModalBackdrop(props) { 21 | const { classes, open } = props; 22 | return ; 23 | } 24 | 25 | ModalBackdrop.propTypes = { 26 | classes: PropTypes.object.isRequired, 27 | open: PropTypes.bool.isRequired 28 | }; 29 | 30 | export default withStyles(styles)(ModalBackdrop); 31 | -------------------------------------------------------------------------------- /src/shared/components/NavigationDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { 5 | List, 6 | ListItem, 7 | ListItemIcon, 8 | ListItemText, 9 | Drawer, 10 | IconButton, 11 | Typography, 12 | Toolbar, 13 | } from "@mui/material"; 14 | import withStyles from "@mui/styles/withStyles"; 15 | import CloseIcon from "@mui/icons-material/Close"; 16 | import useMediaQuery from "@mui/material/useMediaQuery"; 17 | 18 | const styles = (theme) => ({ 19 | closeIcon: { 20 | marginRight: theme.spacing(0.5), 21 | }, 22 | headSection: { 23 | width: 200, 24 | }, 25 | blackList: { 26 | backgroundColor: theme.palette.common.black, 27 | height: "100%", 28 | }, 29 | noDecoration: { 30 | textDecoration: "none !important", 31 | }, 32 | }); 33 | 34 | function NavigationDrawer(props) { 35 | const { open, onClose, anchor, classes, menuItems, selectedItem, theme } = 36 | props; 37 | const isWidthUpSm = useMediaQuery(theme.breakpoints.up("sm")); 38 | 39 | window.onresize = () => { 40 | if (isWidthUpSm && open) { 41 | onClose(); 42 | } 43 | }; 44 | 45 | return ( 46 | 47 | 48 | 57 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {menuItems.map((element) => { 70 | if (element.link) { 71 | return ( 72 | 78 | 88 | {element.icon} 89 | 92 | {element.name} 93 | 94 | } 95 | /> 96 | 97 | 98 | ); 99 | } 100 | return ( 101 | 102 | {element.icon} 103 | 106 | {element.name} 107 | 108 | } 109 | /> 110 | 111 | ); 112 | })} 113 | 114 | 115 | ); 116 | } 117 | 118 | NavigationDrawer.propTypes = { 119 | anchor: PropTypes.string.isRequired, 120 | theme: PropTypes.object.isRequired, 121 | open: PropTypes.bool.isRequired, 122 | onClose: PropTypes.func.isRequired, 123 | menuItems: PropTypes.arrayOf(PropTypes.object).isRequired, 124 | classes: PropTypes.object.isRequired, 125 | selectedItem: PropTypes.string, 126 | }; 127 | 128 | export default withStyles(styles, { withTheme: true })(NavigationDrawer); 129 | -------------------------------------------------------------------------------- /src/shared/components/PropsRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Route } from "react-router-dom"; 4 | 5 | const renderMergedProps = (component, ...rest) => { 6 | const finalProps = Object.assign({}, ...rest); 7 | return React.createElement(component, finalProps); 8 | }; 9 | 10 | /** 11 | * Wrapper around the Router component, which makes it pass the properties 12 | * to it's child. 13 | * Taken from https://github.com/ReactTraining/react-router/issues/4105 14 | */ 15 | const PropsRoute = ({ component, ...rest }) => ( 16 | renderMergedProps(component, routeProps, rest)} 19 | /> 20 | ); 21 | 22 | PropsRoute.propTypes = { 23 | component: PropTypes.oneOfType([PropTypes.elementType, PropTypes.node]) 24 | }; 25 | 26 | export default PropsRoute; 27 | -------------------------------------------------------------------------------- /src/shared/components/SelfAligningImage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import format from "date-fns/format"; 4 | import { ImageListItemBar } from "@mui/material"; 5 | import withStyles from '@mui/styles/withStyles'; 6 | import VertOptions from "./VertOptions"; 7 | 8 | const styles = { 9 | imageContainer: { 10 | width: "100%", 11 | paddingTop: "100%", 12 | overflow: "hidden", 13 | position: "relative", 14 | }, 15 | image: { 16 | position: "absolute", 17 | top: 0, 18 | bottom: 0, 19 | left: 0, 20 | right: 0, 21 | margin: "auto", 22 | }, 23 | }; 24 | 25 | function SelfAligningImage(props) { 26 | const { 27 | classes, 28 | src, 29 | title, 30 | timeStamp, 31 | options, 32 | roundedBorder, 33 | theme, 34 | } = props; 35 | const img = useRef(); 36 | const [hasMoreWidthThanHeight, setHasMoreWidthThanHeight] = useState(null); 37 | const [hasLoaded, setHasLoaded] = useState(false); 38 | 39 | const onLoad = useCallback(() => { 40 | if (img.current.naturalHeight < img.current.naturalWidth) { 41 | setHasMoreWidthThanHeight(true); 42 | } else { 43 | setHasMoreWidthThanHeight(false); 44 | } 45 | setHasLoaded(true); 46 | }, [img, setHasLoaded, setHasMoreWidthThanHeight]); 47 | 48 | return ( 49 |
50 | 63 | {title && ( 64 | 0 && ( 71 | 72 | ) 73 | } 74 | /> 75 | )} 76 |
77 | ); 78 | } 79 | 80 | SelfAligningImage.propTypes = { 81 | classes: PropTypes.object.isRequired, 82 | src: PropTypes.string.isRequired, 83 | theme: PropTypes.object.isRequired, 84 | title: PropTypes.string, 85 | timeStamp: PropTypes.number, 86 | roundedBorder: PropTypes.bool, 87 | options: PropTypes.arrayOf(PropTypes.object), 88 | }; 89 | 90 | export default withStyles(styles, { withTheme: true })(SelfAligningImage); 91 | -------------------------------------------------------------------------------- /src/shared/components/VertOptions.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Popover, IconButton, MenuList, ListItemText, ListItemIcon, MenuItem } from "@mui/material"; 4 | import withStyles from '@mui/styles/withStyles'; 5 | import MoreVertIcon from "@mui/icons-material/MoreVert"; 6 | 7 | const styles = { 8 | listItemtext: { 9 | paddingLeft: "0 !important", 10 | }, 11 | }; 12 | 13 | function VertOptions(props) { 14 | const { items, classes, color } = props; 15 | const anchorEl = useRef(); 16 | const [isOpen, setIsOpen] = useState(false); 17 | 18 | const handleClose = useCallback(() => { 19 | setIsOpen(false); 20 | }, [setIsOpen]); 21 | 22 | const handleOpen = useCallback(() => { 23 | setIsOpen(true); 24 | }, [setIsOpen]); 25 | 26 | const id = isOpen ? "scroll-playground" : null; 27 | return ( 28 | 29 | 36 | 37 | 38 | 53 | 54 | {items.map((item) => ( 55 | { 58 | handleClose(); 59 | item.onClick(); 60 | }} 61 | > 62 | {item.icon} 63 | 64 | {item.name} 65 | 66 | 67 | ))} 68 | 69 | 70 | 71 | ); 72 | } 73 | 74 | VertOptions.propTypes = { 75 | items: PropTypes.arrayOf(PropTypes.object).isRequired, 76 | classes: PropTypes.object.isRequired, 77 | color: PropTypes.string, 78 | }; 79 | 80 | export default withStyles(styles)(VertOptions); 81 | -------------------------------------------------------------------------------- /src/shared/components/VisibilityPasswordTextField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField, InputAdornment, IconButton } from "@mui/material"; 3 | import VisibilityIcon from "@mui/icons-material/Visibility"; 4 | import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; 5 | 6 | function VisibilityPasswordTextField(props) { 7 | const { isVisible, onVisibilityChange, ...rest } = props; 8 | return ( 9 | 15 | { 18 | onVisibilityChange(!isVisible); 19 | }} 20 | onMouseDown={(event) => { 21 | event.preventDefault(); 22 | }} 23 | size="large"> 24 | {isVisible ? : } 25 | 26 | 27 | ), 28 | }} 29 | > 30 | ); 31 | } 32 | 33 | export default VisibilityPasswordTextField; 34 | -------------------------------------------------------------------------------- /src/shared/components/WaveBorder.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from '@mui/styles/withStyles'; 4 | 5 | const styles = { 6 | waves: { 7 | position: "relative", 8 | width: "100%", 9 | marginBottom: -7, 10 | height: "7vw", 11 | minHeight: "7vw" 12 | }, 13 | "@keyframes moveForever": { 14 | from: { transform: "translate3d(-90px, 0, 0)" }, 15 | to: { transform: "translate3d(85px, 0, 0)" } 16 | }, 17 | parallax: { 18 | "& > use": { 19 | animation: "$moveForever 4s cubic-bezier(0.62, 0.5, 0.38, 0.5) infinite", 20 | animationDelay: props => `-${props.animationNegativeDelay}s` 21 | } 22 | } 23 | }; 24 | 25 | /** 26 | * https://codepen.io/csspoints/pen/WNeOEqd 27 | */ 28 | function WaveBorder(props) { 29 | const id = String(Math.random()); 30 | const { 31 | className, 32 | lowerColor, 33 | upperColor, 34 | classes, 35 | animationNegativeDelay, 36 | ...rest 37 | } = props; 38 | return ( 39 |
40 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | ); 60 | } 61 | 62 | WaveBorder.propTypes = { 63 | lowerColor: PropTypes.string.isRequired, 64 | upperColor: PropTypes.string.isRequired, 65 | classes: PropTypes.object.isRequired, 66 | animationNegativeDelay: PropTypes.number.isRequired 67 | }; 68 | 69 | export default withStyles(styles)(WaveBorder); 70 | -------------------------------------------------------------------------------- /src/shared/components/ZoomImage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Portal, Backdrop } from "@mui/material"; 4 | import withStyles from "@mui/styles/withStyles"; 5 | import ScrollbarSize from "@mui/material/Tabs/ScrollbarSize"; 6 | import classNames from "classnames"; 7 | 8 | const styles = (theme) => ({ 9 | backdrop: { 10 | zIndex: theme.zIndex.modal, 11 | backgroundColor: "rgba(0, 0, 0, 0.8)", 12 | }, 13 | portalImgWrapper: { 14 | position: "fixed", 15 | top: "0", 16 | left: "0", 17 | width: "100%", 18 | height: "100%", 19 | zIndex: theme.zIndex.modal, 20 | cursor: "pointer", 21 | }, 22 | portalImgInnerWrapper: { 23 | display: "flex", 24 | justifyContent: "center", 25 | alignItems: "center", 26 | width: "100%", 27 | height: "100%", 28 | paddingLeft: theme.spacing(1), 29 | paddingRight: theme.spacing(1), 30 | paddingTop: theme.spacing(1), 31 | paddingBottom: theme.spacing(1), 32 | }, 33 | portalImg: { 34 | objectFit: "contain", 35 | maxWidth: "100%", 36 | maxHeight: "100%", 37 | }, 38 | zoomedOutImage: { 39 | cursor: "pointer", 40 | }, 41 | }); 42 | 43 | function ZoomImage(props) { 44 | const { alt, src, zoomedImgProps, classes, className, ...rest } = props; 45 | const [zoomedIn, setZoomedIn] = useState(false); 46 | const [scrollbarSize, setScrollbarSize] = useState(null); 47 | 48 | const zoomIn = useCallback(() => { 49 | setZoomedIn(true); 50 | }, [setZoomedIn]); 51 | 52 | const zoomOut = useCallback(() => { 53 | setZoomedIn(false); 54 | }, [setZoomedIn]); 55 | 56 | useEffect(() => { 57 | if (zoomedIn) { 58 | document.body.style.overflow = "hidden"; 59 | document.body.style.paddingRight = `${scrollbarSize}px`; 60 | document.querySelector( 61 | "header" 62 | ).style.paddingRight = `${scrollbarSize}px`; 63 | } else { 64 | document.body.style.overflow = "auto"; 65 | document.body.style.paddingRight = "0px"; 66 | document.querySelector("header").style.paddingRight = "0px"; 67 | } 68 | }, [zoomedIn, scrollbarSize]); 69 | 70 | return ( 71 | 72 | 73 | {zoomedIn && ( 74 | 75 | 80 |
81 |
82 | {alt} 88 |
89 |
90 |
91 | )} 92 | {alt} 99 |
100 | ); 101 | } 102 | 103 | ZoomImage.propTypes = { 104 | classes: PropTypes.object.isRequired, 105 | alt: PropTypes.string.isRequired, 106 | src: PropTypes.string.isRequired, 107 | theme: PropTypes.object.isRequired, 108 | zoomedImgProps: PropTypes.object, 109 | className: PropTypes.string, 110 | }; 111 | 112 | export default withStyles(styles, { withTheme: true })(ZoomImage); 113 | -------------------------------------------------------------------------------- /src/shared/functions/countWithEmojis.js: -------------------------------------------------------------------------------- 1 | import toArray from "./toArray"; 2 | 3 | /** 4 | * Counts the characters in a string and counts emojis correctly. 5 | * 6 | * @param {string} str The string to count characters from. 7 | * @return {number} The number of characters in the string. 8 | */ 9 | function countWithEmojis(str) { 10 | return toArray(str).length; 11 | } 12 | 13 | export default countWithEmojis; 14 | -------------------------------------------------------------------------------- /src/shared/functions/currencyPrettyPrint.js: -------------------------------------------------------------------------------- 1 | function currencyPrettyPrint(cents) { 2 | const dollars = cents / 100; 3 | return dollars.toLocaleString("en-US", { 4 | style: "currency", 5 | currency: "USD" 6 | }); 7 | } 8 | 9 | export default currencyPrettyPrint; 10 | -------------------------------------------------------------------------------- /src/shared/functions/getSorting.js: -------------------------------------------------------------------------------- 1 | function desc(a, b, orderBy) { 2 | if (b[orderBy] < a[orderBy]) { 3 | return -1; 4 | } 5 | if (b[orderBy] > a[orderBy]) { 6 | return 1; 7 | } 8 | return 0; 9 | } 10 | 11 | function getSorting(order, orderBy) { 12 | return order === "desc" 13 | ? (a, b) => desc(a, b, orderBy) 14 | : (a, b) => -desc(a, b, orderBy); 15 | } 16 | export default getSorting; 17 | -------------------------------------------------------------------------------- /src/shared/functions/shadeColor.js: -------------------------------------------------------------------------------- 1 | function shadeColor(color, percent) { 2 | const f = parseInt(color.slice(1), 16); 3 | const t = percent < 0 ? 0 : 255; 4 | const p = percent < 0 ? percent * -1 : percent; 5 | const R = f >> 16; 6 | const G = (f >> 8) & 0x00ff; 7 | const B = f & 0x0000ff; 8 | return `#${( 9 | 0x1000000 + 10 | (Math.round((t - R) * p) + R) * 0x10000 + 11 | (Math.round((t - G) * p) + G) * 0x100 + 12 | (Math.round((t - B) * p) + B) 13 | ) 14 | .toString(16) 15 | .slice(1)}`; 16 | } 17 | 18 | export default shadeColor; 19 | -------------------------------------------------------------------------------- /src/shared/functions/smoothScrollTop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * When called scrolls smooth to the top of the page. 3 | * globLastC prevents shaky animations when scrolling to 4 | * bottom while topscrolling. 5 | */ 6 | let globLastC = Infinity; 7 | 8 | function smoothScrollTopRec() { 9 | const c = document.documentElement.scrollTop || document.body.scrollTop; 10 | if (c > 0 && globLastC > c) { 11 | globLastC = c; 12 | window.requestAnimationFrame(smoothScrollTopRec); 13 | window.scrollTo(0, c - c / 8); 14 | } else { 15 | globLastC = Infinity; 16 | } 17 | } 18 | 19 | function smoothScrollTop() { 20 | /** 21 | * Normally this gets called from componentDidMount() 22 | * Not waiting a tiny fraction of time can lead 23 | * to shaky behaviour 24 | */ 25 | setTimeout(() => { 26 | smoothScrollTopRec(); 27 | }, 10); 28 | } 29 | 30 | export default smoothScrollTop; 31 | -------------------------------------------------------------------------------- /src/shared/functions/stableSort.js: -------------------------------------------------------------------------------- 1 | function stableSort(array, cmp) { 2 | const stabilizedThis = array.map((el, index) => [el, index]); 3 | stabilizedThis.sort((a, b) => { 4 | const order = cmp(a[0], b[0]); 5 | if (order !== 0) return order; 6 | return a[1] - b[1]; 7 | }); 8 | return stabilizedThis.map(el => el[0]); 9 | } 10 | export default stableSort; 11 | -------------------------------------------------------------------------------- /src/shared/functions/unixToDateString.js: -------------------------------------------------------------------------------- 1 | function unixToDateString(unix) { 2 | const date = new Date(unix * 1000); 3 | return ( 4 | `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}` 5 | ); 6 | } 7 | 8 | export default unixToDateString; 9 | -------------------------------------------------------------------------------- /src/shared/functions/useLocationBlocker.js: -------------------------------------------------------------------------------- 1 | import { useHistory } from "react-router-dom"; 2 | import { useEffect } from "react"; 3 | 4 | 5 | const useLocationBlocker = () => { 6 | /** 7 | * Prevents react-router from pushing the same 8 | * page to the history twice which leads to 9 | * multiple clicks on the back icon of the browser 10 | * being necessary to go back into the history. 11 | */ 12 | const history = useHistory(); 13 | useEffect( 14 | () => 15 | history.block( 16 | (location, action) => 17 | action !== "PUSH" || 18 | getLocationId(location) !== getLocationId(history.location) 19 | ), 20 | [] // eslint-disable-line react-hooks/exhaustive-deps 21 | ); 22 | } 23 | 24 | const getLocationId = ({ pathname, search, hash }) => { 25 | return pathname + (search ? "?" + search : "") + (hash ? "#" + hash : ""); 26 | } 27 | 28 | export default useLocationBlocker; -------------------------------------------------------------------------------- /src/shared/functions/useWidth.js: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@mui/material/styles"; 2 | import useMediaQuery from "@mui/material/useMediaQuery"; 3 | 4 | /** 5 | * Be careful using this hook. It only works because the number of 6 | * breakpoints in theme is static. It will break once you change the number of 7 | * breakpoints. See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level 8 | */ 9 | function useWidth() { 10 | const theme = useTheme(); 11 | const keys = [...theme.breakpoints.keys].reverse(); 12 | return ( 13 | keys.reduce((output, key) => { 14 | // eslint-disable-next-line react-hooks/rules-of-hooks 15 | const matches = useMediaQuery(theme.breakpoints.up(key)); 16 | return !output && matches ? key : output; 17 | }, null) || "xs" 18 | ); 19 | } 20 | 21 | export default useWidth; 22 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme, responsiveFontSizes, adaptV4Theme } from "@mui/material"; 2 | 3 | // colors 4 | const primary = "#b3294e"; 5 | const secondary = "#4829B2"; 6 | const black = "#343a40"; 7 | const darkBlack = "rgb(36, 40, 44)"; 8 | const background = "#f5f5f5"; 9 | const warningLight = "rgba(253, 200, 69, .3)"; 10 | const warningMain = "rgba(253, 200, 69, .5)"; 11 | const warningDark = "rgba(253, 200, 69, .7)"; 12 | 13 | // border 14 | const borderWidth = 2; 15 | const borderColor = "rgba(0, 0, 0, 0.13)"; 16 | 17 | // breakpoints 18 | const xl = 1920; 19 | const lg = 1280; 20 | const md = 960; 21 | const sm = 600; 22 | const xs = 0; 23 | 24 | // spacing 25 | const spacing = 8; 26 | 27 | const theme = createTheme(adaptV4Theme({ 28 | palette: { 29 | primary: { main: primary }, 30 | secondary: { main: secondary }, 31 | common: { 32 | black, 33 | darkBlack 34 | }, 35 | warning: { 36 | light: warningLight, 37 | main: warningMain, 38 | dark: warningDark 39 | }, 40 | // Used to shift a color's luminance by approximately 41 | // two indexes within its tonal palette. 42 | // E.g., shift from Red 500 to Red 300 or Red 700. 43 | tonalOffset: 0.2, 44 | background: { 45 | default: background 46 | }, 47 | spacing 48 | }, 49 | breakpoints: { 50 | // Define custom breakpoint values. 51 | // These will apply to Material-UI components that use responsive 52 | // breakpoints, such as `Grid` and `Hidden`. You can also use the 53 | // theme breakpoint functions `up`, `down`, and `between` to create 54 | // media queries for these breakpoints 55 | values: { 56 | xl, 57 | lg, 58 | md, 59 | sm, 60 | xs 61 | } 62 | }, 63 | border: { 64 | borderColor: borderColor, 65 | borderWidth: borderWidth 66 | }, 67 | overrides: { 68 | MuiExpansionPanel: { 69 | root: { 70 | position: "static" 71 | } 72 | }, 73 | MuiTableCell: { 74 | root: { 75 | paddingLeft: spacing * 2, 76 | paddingRight: spacing * 2, 77 | borderBottom: `${borderWidth}px solid ${borderColor}`, 78 | [`@media (max-width: ${sm}px)`]: { 79 | paddingLeft: spacing, 80 | paddingRight: spacing 81 | } 82 | } 83 | }, 84 | MuiDivider: { 85 | root: { 86 | backgroundColor: borderColor, 87 | height: borderWidth 88 | } 89 | }, 90 | MuiPrivateNotchedOutline: { 91 | root: { 92 | borderWidth: borderWidth 93 | } 94 | }, 95 | MuiListItem: { 96 | divider: { 97 | borderBottom: `${borderWidth}px solid ${borderColor}` 98 | } 99 | }, 100 | MuiDialog: { 101 | paper: { 102 | width: "100%", 103 | maxWidth: 430, 104 | marginLeft: spacing, 105 | marginRight: spacing 106 | } 107 | }, 108 | MuiTooltip: { 109 | tooltip: { 110 | backgroundColor: darkBlack 111 | } 112 | }, 113 | MuiExpansionPanelDetails: { 114 | root: { 115 | [`@media (max-width: ${sm}px)`]: { 116 | paddingLeft: spacing, 117 | paddingRight: spacing 118 | } 119 | } 120 | } 121 | }, 122 | typography: { 123 | useNextVariants: true 124 | } 125 | })); 126 | 127 | export default responsiveFontSizes(theme); 128 | --------------------------------------------------------------------------------