├── .env ├── .github ├── 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 ├── serviceWorker.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 └── theme.js /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=src/ 2 | 3 | -------------------------------------------------------------------------------- /.github/gifs/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/.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 | [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=dunky11/react-saas-template)](https://dependabot.com) 8 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 9 | 10 | [](https://reactsaastemplate.com "Go to demo website") 11 | 12 | 13 | ## Getting Started 14 | 15 | ### Prerequisites 16 | 17 | #### Node.js 12+ (versions below could work, but are not tested) 18 | 19 | * Linux: 20 | 21 | ``` 22 | sudo apt install nodejs npm 23 | ``` 24 | 25 | * Windows or macOS: 26 | 27 | https://nodejs.org/en/ 28 | 29 | ### Installing 30 | 31 | 1. Clone the repository 32 | 33 | ``` 34 | git clone https://github.com/dunky11/react-saas-template 35 | ``` 36 | 2. Install dependencies, this can take a minute 37 | 38 | ``` 39 | cd react-saas-template 40 | npm install 41 | ``` 42 | 3. Start the local server 43 | 44 | ``` 45 | npm start 46 | ``` 47 | 48 | Your browser should now open and show the app. Otherwise open http://localhost:3000/ in your browser. Editing files will automatically refresh the page. 49 | 50 | ### What to do next? 51 | 52 | If you are new to React, you should watch a [basic React tutorial](https://www.youtube.com/results?search_query=react+tutorial) first. 53 | 54 | If you already know React, then most of the information you need is in the [Material-UI documentation](https://material-ui.com/getting-started/usage/). 55 | 56 | 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. 57 | 58 | ## Deployment 59 | 60 | If you are happy with the state of your website you can run: 61 | 62 | ``` 63 | npm run build 64 | ``` 65 | 66 | 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. 67 | 68 | ## Build With 69 | 70 | * [Create-React-App](https://github.com/facebook/create-react-app) - Used to bootstrap the development 71 | * [Material-UI](https://github.com/mui-org/material-ui) - Material Design components 72 | * [React-Router](https://github.com/ReactTraining/react-router) - Routing of the app 73 | * [Pace](https://github.com/HubSpot/pace) - Loading bar at the top 74 | * [Emoji-Mart](https://github.com/missive/emoji-mart) - Picker for the emojis 75 | * [React-Dropzone](https://github.com/react-dropzone/react-dropzone) - File drop component for uploads 76 | * [Recharts](https://github.com/recharts/recharts) - Charting library I used for the statistics 77 | * [Aos](https://github.com/michalsnik/aos) - Animations based on viewport 78 | * [React-Cropper](https://github.com/roadmanfong/react-cropper) - Cropper for the image uploads 79 | * [React-Stripe-js](https://github.com/stripe/react-stripe-js) - Stripes payment elements 80 | 81 | ## Things im currently working on 82 | 83 | - [ ] Improving the encapsulation of components 84 | - [ ] smoothScrollTop() sometimes stops scrolling top when components with big height are still rendering 85 | - [ ] When a Dialog is opened there is a margin on the right side of the viewport (could be that this is not fixable without shaking the viewport on dialog open) 86 | - [ ] shadeColor() throws errors on certain color codes 87 | - [ ] Adding iDEAL, FBX and PaymentRequestButton to avaible payment methods 88 | 89 | ## Contribute 90 | Show your support by ⭐ the project. Pull requests are always welcome. 91 | 92 | ## License 93 | 94 | 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. 95 | -------------------------------------------------------------------------------- /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 | "@material-ui/core": "^4.11.0", 10 | "@material-ui/icons": "^4.9.1", 11 | "@material-ui/pickers": "^3.2.10", 12 | "@material-ui/system": "^4.9.14", 13 | "@stripe/react-stripe-js": "^1.1.2", 14 | "@stripe/stripe-js": "^1.10.0", 15 | "aos": "^2.3.4", 16 | "classnames": "^2.2.6", 17 | "date-fns": "^2.16.1", 18 | "emoji-mart": "^3.0.0", 19 | "js-cookie": "^2.2.0", 20 | "prop-types": "^15.7.2", 21 | "react": "^16.14.0", 22 | "react-cropper": "^2.1.4", 23 | "react-dom": "^16.14.0", 24 | "react-dropzone": "^11.2.0", 25 | "react-router": "^5.2.0", 26 | "react-router-dom": "^5.2.0", 27 | "react-scripts": "^3.4.4", 28 | "recharts": "^1.8.5" 29 | }, 30 | "devDependencies": { 31 | "@testing-library/jest-dom": "^5.11.4", 32 | "@testing-library/react": "^11.1.0", 33 | "@testing-library/user-event": "^12.1.10" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/dunky11/react-saas-template.git" 59 | }, 60 | "author": "dunky11", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/dunky11/react-saas-template/issues" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/favicon-512x512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logged_in/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image1.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image10.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image2.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image3.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image4.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image5.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image6.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image7.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image8.jpg -------------------------------------------------------------------------------- /public/images/logged_in/image9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/image9.jpg -------------------------------------------------------------------------------- /public/images/logged_in/profilePicture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_in/profilePicture.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost1.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost2.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost3.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost4.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost5.jpg -------------------------------------------------------------------------------- /public/images/logged_out/blogPost6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/public/images/logged_out/blogPost6.jpg -------------------------------------------------------------------------------- /public/images/logged_out/headerImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srsedev/react-saas-template/b81d3feedd85df12e895eb0ffaaff9e679a45d7a/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 | 50 |
51 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /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 { MuiThemeProvider, CssBaseline } from "@material-ui/core"; 3 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 4 | import theme from "./theme"; 5 | import GlobalStyles from "./GlobalStyles"; 6 | import * as serviceWorker from "./serviceWorker"; 7 | import Pace from "./shared/components/Pace"; 8 | 9 | const LoggedInComponent = lazy(() => import("./logged_in/components/Main")); 10 | 11 | const LoggedOutComponent = lazy(() => import("./logged_out/components/Main")); 12 | 13 | function App() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | }> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | serviceWorker.register(); 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 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)}px !important`, 17 | paddingBottom: `${theme.spacing(1.75)}px !important`, 18 | paddingLeft: `${theme.spacing(4)}px !important`, 19 | [theme.breakpoints.down("sm")]: { 20 | paddingLeft: `${theme.spacing(4)}px !important` 21 | }, 22 | "@media (max-width: 420px)": { 23 | paddingLeft: `${theme.spacing(1)}px !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)}px !important`, 58 | [theme.breakpoints.down("md")]: { 59 | marginTop: `${theme.spacing(18)}px !important` 60 | }, 61 | [theme.breakpoints.down("sm")]: { 62 | marginTop: `${theme.spacing(16)}px !important` 63 | }, 64 | [theme.breakpoints.down("xs")]: { 65 | marginTop: `${theme.spacing(14)}px !important` 66 | } 67 | }, 68 | ".lg-mg-bottom": { 69 | marginBottom: `${theme.spacing(20)}px !important`, 70 | [theme.breakpoints.down("md")]: { 71 | marginBottom: `${theme.spacing(18)}px !important` 72 | }, 73 | [theme.breakpoints.down("sm")]: { 74 | marginBottom: `${theme.spacing(16)}px !important` 75 | }, 76 | [theme.breakpoints.down("xs")]: { 77 | marginBottom: `${theme.spacing(14)}px !important` 78 | } 79 | }, 80 | ".lg-p-top": { 81 | paddingTop: `${theme.spacing(20)}px !important`, 82 | [theme.breakpoints.down("md")]: { 83 | paddingTop: `${theme.spacing(18)}px !important` 84 | }, 85 | [theme.breakpoints.down("sm")]: { 86 | paddingTop: `${theme.spacing(16)}px !important` 87 | }, 88 | [theme.breakpoints.down("xs")]: { 89 | paddingTop: `${theme.spacing(14)}px !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 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById("root") 8 | ); 9 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 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 | 10 | const styles = (theme) => ({ 11 | wrapper: { 12 | margin: theme.spacing(1), 13 | width: "auto", 14 | [theme.breakpoints.up("xs")]: { 15 | width: "95%", 16 | marginLeft: "auto", 17 | marginRight: "auto", 18 | marginTop: theme.spacing(4), 19 | marginBottom: theme.spacing(4), 20 | }, 21 | [theme.breakpoints.up("sm")]: { 22 | marginTop: theme.spacing(6), 23 | marginBottom: theme.spacing(6), 24 | width: "90%", 25 | marginLeft: "auto", 26 | marginRight: "auto", 27 | }, 28 | [theme.breakpoints.up("md")]: { 29 | marginTop: theme.spacing(6), 30 | marginBottom: theme.spacing(6), 31 | width: "82.5%", 32 | marginLeft: "auto", 33 | marginRight: "auto", 34 | }, 35 | [theme.breakpoints.up("lg")]: { 36 | marginTop: theme.spacing(6), 37 | marginBottom: theme.spacing(6), 38 | width: "70%", 39 | marginLeft: "auto", 40 | marginRight: "auto", 41 | }, 42 | }, 43 | }); 44 | 45 | function Routing(props) { 46 | const { 47 | classes, 48 | EmojiTextArea, 49 | ImageCropper, 50 | Dropzone, 51 | DateTimePicker, 52 | pushMessageToSnackbar, 53 | posts, 54 | transactions, 55 | toggleAccountActivation, 56 | CardChart, 57 | statistics, 58 | targets, 59 | setTargets, 60 | setPosts, 61 | isAccountActivated, 62 | selectDashboard, 63 | selectPosts, 64 | selectSubscription, 65 | openAddBalanceDialog, 66 | } = props; 67 | return ( 68 |
69 | 70 | 82 | 90 | 102 | 103 |
104 | ); 105 | } 106 | 107 | Routing.propTypes = { 108 | classes: PropTypes.object.isRequired, 109 | EmojiTextArea: PropTypes.elementType, 110 | ImageCropper: PropTypes.elementType, 111 | Dropzone: PropTypes.elementType, 112 | DateTimePicker: PropTypes.elementType, 113 | pushMessageToSnackbar: PropTypes.func, 114 | setTargets: PropTypes.func.isRequired, 115 | setPosts: PropTypes.func.isRequired, 116 | posts: PropTypes.arrayOf(PropTypes.object).isRequired, 117 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired, 118 | toggleAccountActivation: PropTypes.func, 119 | CardChart: PropTypes.elementType, 120 | statistics: PropTypes.object.isRequired, 121 | targets: PropTypes.arrayOf(PropTypes.object).isRequired, 122 | isAccountActivated: PropTypes.bool.isRequired, 123 | selectDashboard: PropTypes.func.isRequired, 124 | selectPosts: PropTypes.func.isRequired, 125 | selectSubscription: PropTypes.func.isRequired, 126 | openAddBalanceDialog: PropTypes.func.isRequired, 127 | }; 128 | 129 | export default withStyles(styles, { withTheme: true })(memo(Routing)); 130 | -------------------------------------------------------------------------------- /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 | withStyles 13 | } from "@material-ui/core"; 14 | import LoopIcon from "@material-ui/icons/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 "@material-ui/core"; 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, withTheme } from "@material-ui/core"; 4 | 5 | function StatisticsArea(props) { 6 | const { theme, CardChart, data } = props; 7 | return ( 8 | CardChart && 9 | data.profit.length >= 2 && 10 | data.views.length >= 2 && ( 11 | 12 | 13 | 19 | 20 | 21 | 27 | 28 | 29 | ) 30 | ); 31 | } 32 | 33 | StatisticsArea.propTypes = { 34 | theme: PropTypes.object.isRequired, 35 | data: PropTypes.object.isRequired, 36 | CardChart: PropTypes.elementType 37 | }; 38 | 39 | export default withTheme(StatisticsArea); 40 | -------------------------------------------------------------------------------- /src/logged_in/components/navigation/Balance.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { OutlinedInput, withStyles } from "@material-ui/core"; 4 | import currencyPrettyPrint from "../../../shared/functions/currencyPrettyPrint"; 5 | 6 | const styles = { 7 | input: { padding: "0px 9px", cursor: "pointer" }, 8 | outlinedInput: { 9 | width: 90, 10 | height: 40, 11 | cursor: "pointer" 12 | }, 13 | wrapper: { 14 | display: "flex", 15 | alignItems: "center" 16 | } 17 | }; 18 | 19 | function Balance(props) { 20 | const { balance, classes, openAddBalanceDialog } = props; 21 | return ( 22 |
23 | 31 |
32 | ); 33 | } 34 | 35 | Balance.propTypes = { 36 | balance: PropTypes.number.isRequired, 37 | classes: PropTypes.object.isRequired, 38 | openAddBalanceDialog: PropTypes.func.isRequired 39 | }; 40 | 41 | export default withStyles(styles)(Balance); 42 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 9 | import ErrorIcon from "@material-ui/icons/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 | withStyles, 14 | } from "@material-ui/core"; 15 | import MessageIcon from "@material-ui/icons/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("sm")]: { 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 { 4 | Drawer, 5 | IconButton, 6 | Toolbar, 7 | Divider, 8 | Typography, 9 | Box, 10 | withStyles 11 | } from "@material-ui/core"; 12 | import CloseIcon from "@material-ui/icons/Close"; 13 | 14 | const drawerWidth = 240; 15 | 16 | const styles = { 17 | toolbar: { 18 | minWidth: drawerWidth 19 | } 20 | }; 21 | 22 | function SideDrawer(props) { 23 | const { classes, onClose, open } = props; 24 | return ( 25 | 26 | 27 | 35 | A Sidedrawer 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | SideDrawer.propTypes = { 51 | classes: PropTypes.object.isRequired, 52 | open: PropTypes.bool.isRequired, 53 | onClose: PropTypes.func.isRequired 54 | }; 55 | 56 | export default withStyles(styles)(SideDrawer); 57 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 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 { 4 | Grid, 5 | TablePagination, 6 | Divider, 7 | Toolbar, 8 | Typography, 9 | Button, 10 | Paper, 11 | Box, 12 | withStyles, 13 | } from "@material-ui/core"; 14 | import DeleteIcon from "@material-ui/icons/Delete"; 15 | import SelfAligningImage from "../../../shared/components/SelfAligningImage"; 16 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 17 | import ConfirmationDialog from "../../../shared/components/ConfirmationDialog"; 18 | 19 | const styles = { 20 | dBlock: { display: "block" }, 21 | dNone: { display: "none" }, 22 | toolbar: { 23 | justifyContent: "space-between", 24 | }, 25 | }; 26 | 27 | const rowsPerPage = 25; 28 | 29 | function PostContent(props) { 30 | const { 31 | pushMessageToSnackbar, 32 | setPosts, 33 | posts, 34 | openAddPostModal, 35 | classes, 36 | } = props; 37 | const [page, setPage] = useState(0); 38 | const [isDeletePostDialogOpen, setIsDeletePostDialogOpen] = useState(false); 39 | const [isDeletePostDialogLoading, setIsDeletePostDialogLoading] = useState( 40 | false 41 | ); 42 | 43 | const closeDeletePostDialog = useCallback(() => { 44 | setIsDeletePostDialogOpen(false); 45 | setIsDeletePostDialogLoading(false); 46 | }, [setIsDeletePostDialogOpen, setIsDeletePostDialogLoading]); 47 | 48 | const deletePost = useCallback(() => { 49 | setIsDeletePostDialogLoading(true); 50 | setTimeout(() => { 51 | const _posts = [...posts]; 52 | const index = _posts.find((element) => element.id === deletePost.id); 53 | _posts.splice(index, 1); 54 | setPosts(_posts); 55 | pushMessageToSnackbar({ 56 | text: "Your post has been deleted", 57 | }); 58 | closeDeletePostDialog(); 59 | }, 1500); 60 | }, [ 61 | posts, 62 | setPosts, 63 | setIsDeletePostDialogLoading, 64 | pushMessageToSnackbar, 65 | closeDeletePostDialog, 66 | ]); 67 | 68 | const onDelete = useCallback(() => { 69 | setIsDeletePostDialogOpen(true); 70 | }, [setIsDeletePostDialogOpen]); 71 | 72 | const handleChangePage = useCallback( 73 | (__, page) => { 74 | setPage(page); 75 | }, 76 | [setPage] 77 | ); 78 | 79 | const printImageGrid = useCallback(() => { 80 | if (posts.length > 0) { 81 | return ( 82 | 83 | 84 | {posts 85 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 86 | .map((post) => ( 87 | 88 | { 96 | onDelete(post); 97 | }, 98 | icon: , 99 | }, 100 | ]} 101 | /> 102 | 103 | ))} 104 | 105 | 106 | ); 107 | } 108 | return ( 109 | 110 | 111 | No posts added yet. Click on "NEW" to create your first one. 112 | 113 | 114 | ); 115 | }, [posts, onDelete, page]); 116 | 117 | return ( 118 | 119 | 120 | Your Posts 121 | 129 | 130 | 131 | {printImageGrid()} 132 | 0 ? classes.dBlock : classes.dNone, 148 | caption: posts.length > 0 ? classes.dBlock : classes.dNone, 149 | }} 150 | labelRowsPerPage="" 151 | /> 152 | 160 | 161 | ); 162 | } 163 | 164 | PostContent.propTypes = { 165 | openAddPostModal: PropTypes.func.isRequired, 166 | classes: PropTypes.object.isRequired, 167 | posts: PropTypes.arrayOf(PropTypes.object).isRequired, 168 | setPosts: PropTypes.func.isRequired, 169 | pushMessageToSnackbar: PropTypes.func, 170 | }; 171 | 172 | export default withStyles(styles)(PostContent); 173 | -------------------------------------------------------------------------------- /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, withTheme } from "@material-ui/core"; 12 | import StripeCardForm from "./stripe/StripeCardForm"; 13 | import StripeIbanForm from "./stripe/StripeIBANForm"; 14 | import FormDialog from "../../../shared/components/FormDialog"; 15 | import ColoredButton from "../../../shared/components/ColoredButton"; 16 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 17 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 18 | 19 | const stripePromise = loadStripe("pk_test_6pRNASCoBOKtIshFeQd4XMUh"); 20 | 21 | const paymentOptions = ["Credit Card", "SEPA Direct Debit"]; 22 | 23 | const AddBalanceDialog = withTheme(function (props) { 24 | const { open, theme, onClose, onSuccess } = props; 25 | 26 | const [loading, setLoading] = useState(false); 27 | const [paymentOption, setPaymentOption] = useState("Credit Card"); 28 | const [stripeError, setStripeError] = useState(""); 29 | const [name, setName] = useState(""); 30 | const [email, setEmail] = useState(""); 31 | const [amount, setAmount] = useState(0); 32 | const [amountError, setAmountError] = useState(""); 33 | const elements = useElements(); 34 | const stripe = useStripe(); 35 | 36 | const onAmountChange = amount => { 37 | if (amount < 0) { 38 | return; 39 | } 40 | if (amountError) { 41 | setAmountError(""); 42 | } 43 | setAmount(amount); 44 | }; 45 | 46 | const getStripePaymentInfo = () => { 47 | switch (paymentOption) { 48 | case "Credit Card": { 49 | return { 50 | type: "card", 51 | card: elements.getElement(CardElement), 52 | billing_details: { name: name } 53 | }; 54 | } 55 | case "SEPA Direct Debit": { 56 | return { 57 | type: "sepa_debit", 58 | sepa_debit: elements.getElement(IbanElement), 59 | billing_details: { email: email, name: name } 60 | }; 61 | } 62 | default: 63 | throw new Error("No case selected in switch statement"); 64 | } 65 | }; 66 | 67 | const renderPaymentComponent = () => { 68 | switch (paymentOption) { 69 | case "Credit Card": 70 | return ( 71 | 72 | 73 | 82 | 83 | 84 | You can check this integration using the credit card number{" "} 85 | 4242 4242 4242 4242 04 / 24 24 242 42424 86 | 87 | 88 | ); 89 | case "SEPA Direct Debit": 90 | return ( 91 | 92 | 93 | 104 | 105 | 106 | You can check this integration using the IBAN 107 |
108 | DE89370400440532013000 109 |
110 |
111 | ); 112 | default: 113 | throw new Error("No case selected in switch statement"); 114 | } 115 | }; 116 | 117 | return ( 118 | { 125 | event.preventDefault(); 126 | if (amount <= 0) { 127 | setAmountError("Can't be zero"); 128 | return; 129 | } 130 | if (stripeError) { 131 | setStripeError(""); 132 | } 133 | setLoading(true); 134 | const { error } = await stripe.createPaymentMethod( 135 | getStripePaymentInfo() 136 | ); 137 | if (error) { 138 | setStripeError(error.message); 139 | setLoading(false); 140 | return; 141 | } 142 | onSuccess(); 143 | }} 144 | content={ 145 | 146 | 147 | 148 | {paymentOptions.map(option => ( 149 | 150 | { 156 | setStripeError(""); 157 | setPaymentOption(option); 158 | }} 159 | color={theme.palette.common.black} 160 | > 161 | {option} 162 | 163 | 164 | ))} 165 | 166 | 167 | {renderPaymentComponent()} 168 | 169 | } 170 | actions={ 171 | 172 | 182 | 183 | } 184 | /> 185 | ); 186 | }); 187 | 188 | AddBalanceDialog.propTypes = { 189 | open: PropTypes.bool.isRequired, 190 | theme: PropTypes.object.isRequired, 191 | onClose: PropTypes.func.isRequired, 192 | onSuccess: PropTypes.func.isRequired 193 | }; 194 | 195 | function Wrapper(props) { 196 | const { open, onClose, onSuccess } = props; 197 | return ( 198 | 199 | {open && ( 200 | 201 | )} 202 | 203 | ); 204 | } 205 | 206 | 207 | AddBalanceDialog.propTypes = { 208 | open: PropTypes.bool.isRequired, 209 | onClose: PropTypes.func.isRequired, 210 | onSuccess: PropTypes.func.isRequired 211 | }; 212 | 213 | export default Wrapper; 214 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 4 | import SubscriptionTable from "./SubscriptionTable"; 5 | import SubscriptionInfo from "./SubscriptionInfo"; 6 | 7 | const styles = { 8 | divider: { 9 | backgroundColor: "rgba(0, 0, 0, 0.26)" 10 | } 11 | }; 12 | 13 | function Subscription(props) { 14 | const { 15 | transactions, 16 | classes, 17 | openAddBalanceDialog, 18 | selectSubscription 19 | } = props; 20 | 21 | useEffect(selectSubscription, [selectSubscription]); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | Subscription.propTypes = { 35 | classes: PropTypes.object.isRequired, 36 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired, 37 | selectSubscription: PropTypes.func.isRequired, 38 | openAddBalanceDialog: PropTypes.func.isRequired 39 | }; 40 | 41 | export default withStyles(styles)(Subscription); 42 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/SubscriptionInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { ListItemText, Button, Toolbar, withStyles } from "@material-ui/core"; 4 | 5 | const styles = { 6 | toolbar: { 7 | justifyContent: "space-between" 8 | } 9 | }; 10 | 11 | function SubscriptionInfo(props) { 12 | const { classes, openAddBalanceDialog } = props; 13 | return ( 14 | 15 | 16 | 24 | 25 | ); 26 | } 27 | 28 | SubscriptionInfo.propTypes = { 29 | classes: PropTypes.object.isRequired, 30 | openAddBalanceDialog: PropTypes.func.isRequired 31 | }; 32 | 33 | export default withStyles(styles)(SubscriptionInfo); 34 | -------------------------------------------------------------------------------- /src/logged_in/components/subscription/SubscriptionTable.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TablePagination, 8 | TableRow, 9 | withStyles 10 | } from "@material-ui/core"; 11 | import EnhancedTableHead from "../../../shared/components/EnhancedTableHead"; 12 | import ColorfulChip from "../../../shared/components/ColorfulChip"; 13 | import unixToDateString from "../../../shared/functions/unixToDateString"; 14 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 15 | import currencyPrettyPrint from "../../../shared/functions/currencyPrettyPrint"; 16 | 17 | const styles = theme => ({ 18 | tableWrapper: { 19 | overflowX: "auto", 20 | width: "100%" 21 | }, 22 | blackBackground: { 23 | backgroundColor: theme.palette.primary.main 24 | }, 25 | contentWrapper: { 26 | padding: theme.spacing(3), 27 | [theme.breakpoints.down("xs")]: { 28 | padding: theme.spacing(2) 29 | }, 30 | width: "100%" 31 | }, 32 | dBlock: { 33 | display: "block !important" 34 | }, 35 | dNone: { 36 | display: "none !important" 37 | }, 38 | firstData: { 39 | paddingLeft: theme.spacing(3) 40 | } 41 | }); 42 | 43 | const rows = [ 44 | { 45 | id: "description", 46 | numeric: false, 47 | label: "Action" 48 | }, 49 | { 50 | id: "balanceChange", 51 | numeric: false, 52 | label: "Balance change" 53 | }, 54 | { 55 | id: "date", 56 | numeric: false, 57 | label: "Date" 58 | }, 59 | { 60 | id: "paidUntil", 61 | numeric: false, 62 | label: "Paid until" 63 | } 64 | ]; 65 | 66 | const rowsPerPage = 25; 67 | 68 | function SubscriptionTable(props) { 69 | const { transactions, theme, classes } = props; 70 | const [page, setPage] = useState(0); 71 | 72 | const handleChangePage = useCallback( 73 | (_, page) => { 74 | setPage(page); 75 | }, 76 | [setPage] 77 | ); 78 | 79 | if (transactions.length > 0) { 80 | return ( 81 |
82 | 83 | 84 | 85 | {transactions 86 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 87 | .map((transaction, index) => ( 88 | 89 | 94 | {transaction.description} 95 | 96 | 97 | {transaction.balanceChange > 0 ? ( 98 | 104 | ) : ( 105 | 109 | )} 110 | 111 | 112 | {unixToDateString(transaction.timestamp)} 113 | 114 | 115 | {transaction.paidUntil 116 | ? unixToDateString(transaction.paidUntil) 117 | : ""} 118 | 119 | 120 | ))} 121 | 122 |
123 | 0 ? classes.dBlock : classes.dNone, 139 | caption: transactions.length > 0 ? classes.dBlock : classes.dNone 140 | }} 141 | labelRowsPerPage="" 142 | /> 143 |
144 | ); 145 | } 146 | return ( 147 |
148 | 149 | No transactions received yet. 150 | 151 |
152 | ); 153 | } 154 | 155 | SubscriptionTable.propTypes = { 156 | theme: PropTypes.object.isRequired, 157 | classes: PropTypes.object.isRequired, 158 | transactions: PropTypes.arrayOf(PropTypes.object).isRequired 159 | }; 160 | 161 | export default withStyles(styles, { withTheme: true })(SubscriptionTable); 162 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 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 "@material-ui/core"; 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, withTheme } from "@material-ui/core"; 3 | 4 | function MyInputComponent(props) { 5 | const { component: Component, inputRef, ...other } = props; 6 | 7 | // implement `InputElement` interface 8 | React.useImperativeHandle(inputRef, () => ({ 9 | focus: () => { 10 | // logic to focus the rendered component from 3rd party belongs here 11 | } 12 | // hiding the value e.g. react-stripe-elements 13 | })); 14 | 15 | // `Component` will be your `SomeThirdPartyComponent` from below 16 | return ; 17 | } 18 | 19 | function StripeTextField(props) { 20 | const { stripeOptions, StripeElement, select, theme, ...rest } = props; 21 | const options = { 22 | style: { 23 | base: { 24 | ...theme.typography.body1, 25 | color: theme.palette.text.primary, 26 | fontSize: "16px", 27 | fontSmoothing: "antialiased", 28 | "::placeholder": { 29 | color: theme.palette.text.secondary 30 | } 31 | }, 32 | invalid: { 33 | iconColor: theme.palette.error.main, 34 | color: theme.palette.error.main 35 | } 36 | }, 37 | ...stripeOptions 38 | }; 39 | return ( 40 | 50 | ); 51 | } 52 | 53 | export default withTheme(StripeTextField); 54 | -------------------------------------------------------------------------------- /src/logged_in/dummy_data/persons.js: -------------------------------------------------------------------------------- 1 | export default [ 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 | -------------------------------------------------------------------------------- /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 "@material-ui/core"; 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 an 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, []); 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 | 9 | function Routing(props) { 10 | const { blogPosts, selectBlog, selectHome } = props; 11 | return ( 12 | 13 | {blogPosts.map((post) => ( 14 | blogPost.id !== post.id 24 | )} 25 | /> 26 | ))} 27 | 34 | 35 | 36 | ); 37 | } 38 | 39 | Routing.propTypes = { 40 | blogposts: PropTypes.arrayOf(PropTypes.object), 41 | selectHome: PropTypes.func.isRequired, 42 | selectBlog: PropTypes.func.isRequired, 43 | }; 44 | 45 | export default memo(Routing); 46 | -------------------------------------------------------------------------------- /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, isWidthUp, withWidth, withStyles } from "@material-ui/core"; 5 | import BlogCard from "./BlogCard"; 6 | 7 | const styles = (theme) => ({ 8 | blogContentWrapper: { 9 | marginLeft: theme.spacing(1), 10 | marginRight: theme.spacing(1), 11 | [theme.breakpoints.up("sm")]: { 12 | marginLeft: theme.spacing(4), 13 | marginRight: theme.spacing(4), 14 | }, 15 | maxWidth: 1280, 16 | width: "100%", 17 | }, 18 | wrapper: { 19 | minHeight: "60vh", 20 | }, 21 | noDecoration: { 22 | textDecoration: "none !important", 23 | }, 24 | }); 25 | 26 | function getVerticalBlogPosts(width, blogPosts) { 27 | const gridRows = [[], [], []]; 28 | let rows; 29 | let xs; 30 | if (isWidthUp("md", width)) { 31 | rows = 3; 32 | xs = 4; 33 | } else if (isWidthUp("sm", width)) { 34 | rows = 2; 35 | xs = 6; 36 | } else { 37 | rows = 1; 38 | xs = 12; 39 | } 40 | blogPosts.forEach((blogPost, index) => { 41 | gridRows[index % rows].push( 42 | 43 | 44 | 51 | 52 | 53 | ); 54 | }); 55 | return gridRows.map((element, index) => ( 56 | 57 | {element} 58 | 59 | )); 60 | } 61 | 62 | function Blog(props) { 63 | const { classes, width, blogPosts, selectBlog } = props; 64 | 65 | useEffect(() => { 66 | selectBlog(); 67 | }, [selectBlog]); 68 | 69 | return ( 70 | 75 |
76 | 77 | {getVerticalBlogPosts(width, blogPosts)} 78 | 79 |
80 |
81 | ); 82 | } 83 | 84 | Blog.propTypes = { 85 | selectBlog: PropTypes.func.isRequired, 86 | classes: PropTypes.object.isRequired, 87 | width: PropTypes.string.isRequired, 88 | blogPosts: PropTypes.arrayOf(PropTypes.object), 89 | }; 90 | 91 | export default withWidth()(withStyles(styles, { withTheme: true })(Blog)); 92 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 7 | 8 | const styles = (theme) => ({ 9 | img: { 10 | width: "100%", 11 | height: "auto", 12 | marginBottom: 8, 13 | }, 14 | card: { 15 | boxShadow: theme.shadows[2], 16 | }, 17 | noDecoration: { 18 | textDecoration: "none !important", 19 | }, 20 | title: { 21 | transition: theme.transitions.create(["background-color"], { 22 | duration: theme.transitions.duration.complex, 23 | easing: theme.transitions.easing.easeInOut, 24 | }), 25 | cursor: "pointer", 26 | color: theme.palette.secondary.main, 27 | "&:hover": { 28 | color: theme.palette.secondary.dark, 29 | }, 30 | "&:active": { 31 | color: theme.palette.primary.dark, 32 | }, 33 | }, 34 | link: { 35 | transition: theme.transitions.create(["background-color"], { 36 | duration: theme.transitions.duration.complex, 37 | easing: theme.transitions.easing.easeInOut, 38 | }), 39 | cursor: "pointer", 40 | color: theme.palette.primary.main, 41 | "&:hover": { 42 | color: theme.palette.primary.dark, 43 | }, 44 | }, 45 | showFocus: { 46 | "&:focus span": { 47 | color: theme.palette.secondary.dark, 48 | }, 49 | }, 50 | }); 51 | 52 | function BlogCard(props) { 53 | const { classes, url, src, date, title, snippet } = props; 54 | 55 | return ( 56 | 57 | {src && ( 58 | 59 | 60 | 61 | )} 62 | 63 | 64 | {format(new Date(date * 1000), "PPP", { 65 | awareOfUnicodeTokens: true, 66 | })} 67 | 68 | 72 | 73 | {title} 74 | 75 | 76 | 77 | {snippet} 78 | 79 | read more... 80 | 81 | 82 | 83 | 84 | ); 85 | } 86 | 87 | BlogCard.propTypes = { 88 | classes: PropTypes.object.isRequired, 89 | url: PropTypes.string.isRequired, 90 | title: PropTypes.string.isRequired, 91 | date: PropTypes.number.isRequired, 92 | snippet: PropTypes.string.isRequired, 93 | src: PropTypes.string, 94 | }; 95 | 96 | export default withStyles(styles, { withTheme: true })(BlogCard); 97 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 6 | import BlogCard from "./BlogCard"; 7 | import ShareButton from "../../../shared/components/ShareButton"; 8 | import ZoomImage from "../../../shared/components/ZoomImage"; 9 | import smoothScrollTop from "../../../shared/functions/smoothScrollTop"; 10 | 11 | const styles = (theme) => ({ 12 | blogContentWrapper: { 13 | marginLeft: theme.spacing(1), 14 | marginRight: theme.spacing(1), 15 | [theme.breakpoints.up("sm")]: { 16 | marginLeft: theme.spacing(4), 17 | marginRight: theme.spacing(4), 18 | }, 19 | maxWidth: 1280, 20 | width: "100%", 21 | }, 22 | wrapper: { 23 | minHeight: "60vh", 24 | }, 25 | img: { 26 | width: "100%", 27 | height: "auto", 28 | }, 29 | card: { 30 | boxShadow: theme.shadows[4], 31 | }, 32 | }); 33 | 34 | function BlogPost(props) { 35 | const { classes, date, title, src, content, otherArticles } = props; 36 | 37 | useEffect(() => { 38 | document.title = `WaVer - ${title}`; 39 | smoothScrollTop(); 40 | }, [title]); 41 | 42 | return ( 43 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | {title} 55 | 56 | 57 | {format(new Date(date * 1000), "PPP", { 58 | awareOfUnicodeTokens: true, 59 | })} 60 | 61 | 62 | 63 | 64 | {content} 65 | 66 | 67 | {["Facebook", "Twitter", "Reddit", "Tumblr"].map( 68 | (type, index) => ( 69 | 70 | 81 | 82 | ) 83 | )} 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Other articles 92 | 93 | {otherArticles.map((blogPost) => ( 94 | 95 | 101 | 102 | ))} 103 | 104 | 105 |
106 |
107 | ); 108 | } 109 | 110 | BlogPost.propTypes = { 111 | classes: PropTypes.object.isRequired, 112 | title: PropTypes.string.isRequired, 113 | date: PropTypes.number.isRequired, 114 | src: PropTypes.string.isRequired, 115 | content: PropTypes.node.isRequired, 116 | otherArticles: PropTypes.arrayOf(PropTypes.object).isRequired, 117 | }; 118 | 119 | export default withStyles(styles, { withTheme: true })(BlogPost); 120 | -------------------------------------------------------------------------------- /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 { 5 | Snackbar, 6 | Button, 7 | Typography, 8 | Box, 9 | withStyles, 10 | } from "@material-ui/core"; 11 | import fetchIpData from "./fetchIpData"; 12 | 13 | const styles = (theme) => ({ 14 | snackbarContent: { 15 | borderBottomLeftRadius: 0, 16 | borderBottomRightRadius: 0, 17 | paddingLeft: theme.spacing(3), 18 | paddingRight: theme.spacing(3), 19 | }, 20 | }); 21 | 22 | const europeanCountryCodes = [ 23 | "AT", 24 | "BE", 25 | "BG", 26 | "CY", 27 | "CZ", 28 | "DE", 29 | "DK", 30 | "EE", 31 | "ES", 32 | "FI", 33 | "FR", 34 | "GB", 35 | "GR", 36 | "HR", 37 | "HU", 38 | "IE", 39 | "IT", 40 | "LT", 41 | "LU", 42 | "LV", 43 | "MT", 44 | "NL", 45 | "PO", 46 | "PT", 47 | "RO", 48 | "SE", 49 | "SI", 50 | "SK", 51 | ]; 52 | 53 | function CookieConsent(props) { 54 | const { classes, handleCookieRulesDialogOpen } = props; 55 | const [isVisible, setIsVisible] = useState(false); 56 | 57 | const openOnEuCountry = useCallback(() => { 58 | fetchIpData 59 | .then((data) => { 60 | if ( 61 | data && 62 | data.country && 63 | !europeanCountryCodes.includes(data.country) 64 | ) { 65 | setIsVisible(false); 66 | } else { 67 | setIsVisible(true); 68 | } 69 | }) 70 | .catch(() => { 71 | setIsVisible(true); 72 | }); 73 | }, [setIsVisible]); 74 | 75 | /** 76 | * Set a persistent cookie 77 | */ 78 | const onAccept = useCallback(() => { 79 | Cookies.set("remember-cookie-snackbar", "", { 80 | expires: 365, 81 | }); 82 | setIsVisible(false); 83 | }, [setIsVisible]); 84 | 85 | useEffect(() => { 86 | if (Cookies.get("remember-cookie-snackbar") === undefined) { 87 | openOnEuCountry(); 88 | } 89 | }, [openOnEuCountry]); 90 | 91 | return ( 92 | 97 | We use cookies to ensure you get the best experience on our website.{" "} 98 | 99 | } 100 | action={ 101 | 102 | 103 | 106 | 107 | 110 | 111 | } 112 | /> 113 | ); 114 | } 115 | 116 | CookieConsent.propTypes = { 117 | handleCookieRulesDialogOpen: PropTypes.func.isRequired, 118 | }; 119 | 120 | export default withStyles(styles, { withTheme: true })(CookieConsent); 121 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 4 | 5 | const styles = theme => ({ 6 | iconWrapper: { 7 | borderRadius: theme.shape.borderRadius, 8 | textAlign: "center", 9 | display: "inline-flex", 10 | alignItems: "center", 11 | justifyContent: "center", 12 | marginBottom: theme.spacing(3), 13 | padding: theme.spacing(1) * 1.5 14 | } 15 | }); 16 | 17 | function shadeColor(hex, percent) { 18 | const f = parseInt(hex.slice(1), 16); 19 | 20 | const t = percent < 0 ? 0 : 255; 21 | 22 | const p = percent < 0 ? percent * -1 : percent; 23 | 24 | const R = f >> 16; 25 | 26 | const G = (f >> 8) & 0x00ff; 27 | 28 | const B = f & 0x0000ff; 29 | return `#${( 30 | 0x1000000 + 31 | (Math.round((t - R) * p) + R) * 0x10000 + 32 | (Math.round((t - G) * p) + G) * 0x100 + 33 | (Math.round((t - B) * p) + B) 34 | ) 35 | .toString(16) 36 | .slice(1)}`; 37 | } 38 | 39 | function FeatureCard(props) { 40 | const { classes, Icon, color, headline, text } = props; 41 | return ( 42 | 43 |
52 | {Icon} 53 |
54 | 55 | {headline} 56 | 57 | 58 | {text} 59 | 60 |
61 | ); 62 | } 63 | 64 | FeatureCard.propTypes = { 65 | classes: PropTypes.object.isRequired, 66 | Icon: PropTypes.element.isRequired, 67 | color: PropTypes.string.isRequired, 68 | headline: PropTypes.string.isRequired, 69 | text: PropTypes.string.isRequired 70 | }; 71 | 72 | export default withStyles(styles, { withTheme: true })(FeatureCard); 73 | -------------------------------------------------------------------------------- /src/logged_out/components/home/FeatureSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Grid, Typography, isWidthUp, withWidth } from "@material-ui/core"; 4 | import CodeIcon from "@material-ui/icons/Code"; 5 | import BuildIcon from "@material-ui/icons/Build"; 6 | import ComputerIcon from "@material-ui/icons/Computer"; 7 | import BarChartIcon from "@material-ui/icons/BarChart"; 8 | import HeadsetMicIcon from "@material-ui/icons/HeadsetMic"; 9 | import CalendarTodayIcon from "@material-ui/icons/CalendarToday"; 10 | import CloudIcon from "@material-ui/icons/Cloud"; 11 | import MeassageIcon from "@material-ui/icons/Message"; 12 | import CancelIcon from "@material-ui/icons/Cancel"; 13 | import calculateSpacing from "./calculateSpacing"; 14 | import FeatureCard from "./FeatureCard"; 15 | 16 | const iconSize = 30; 17 | 18 | const features = [ 19 | { 20 | color: "#00C853", 21 | headline: "Feature 1", 22 | text: 23 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 24 | icon: , 25 | mdDelay: "0", 26 | smDelay: "0" 27 | }, 28 | { 29 | color: "#6200EA", 30 | headline: "Feature 2", 31 | text: 32 | "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: 41 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 42 | icon: , 43 | mdDelay: "400", 44 | smDelay: "0" 45 | }, 46 | { 47 | color: "#d50000", 48 | headline: "Feature 4", 49 | text: 50 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 51 | icon: , 52 | mdDelay: "0", 53 | smDelay: "200" 54 | }, 55 | { 56 | color: "#DD2C00", 57 | headline: "Feature 5", 58 | text: 59 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 60 | icon: , 61 | mdDelay: "200", 62 | smDelay: "0" 63 | }, 64 | { 65 | color: "#64DD17", 66 | headline: "Feature 6", 67 | text: 68 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 69 | icon: , 70 | mdDelay: "400", 71 | smDelay: "200" 72 | }, 73 | { 74 | color: "#304FFE", 75 | headline: "Feature 7", 76 | text: 77 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 78 | icon: , 79 | mdDelay: "0", 80 | smDelay: "0" 81 | }, 82 | { 83 | color: "#C51162", 84 | headline: "Feature 8", 85 | text: 86 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 87 | icon: , 88 | mdDelay: "200", 89 | smDelay: "200" 90 | }, 91 | { 92 | color: "#00B8D4", 93 | headline: "Feature 9", 94 | text: 95 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et.", 96 | icon: , 97 | mdDelay: "400", 98 | smDelay: "0" 99 | } 100 | ]; 101 | 102 | function FeatureSection(props) { 103 | const { width } = props; 104 | return ( 105 |
106 |
107 | 108 | Features 109 | 110 |
111 | 112 | {features.map(element => ( 113 | 123 | 129 | 130 | ))} 131 | 132 |
133 |
134 |
135 | ); 136 | } 137 | 138 | FeatureSection.propTypes = { 139 | width: PropTypes.string.isRequired 140 | }; 141 | 142 | export default withWidth()(FeatureSection); 143 | -------------------------------------------------------------------------------- /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 { 5 | Grid, 6 | Typography, 7 | Card, 8 | Button, 9 | Hidden, 10 | Box, 11 | withStyles, 12 | withWidth, 13 | isWidthUp, 14 | } from "@material-ui/core"; 15 | import WaveBorder from "../../../shared/components/WaveBorder"; 16 | import ZoomImage from "../../../shared/components/ZoomImage"; 17 | 18 | const styles = (theme) => ({ 19 | extraLargeButtonLabel: { 20 | fontSize: theme.typography.body1.fontSize, 21 | [theme.breakpoints.up("sm")]: { 22 | fontSize: theme.typography.h6.fontSize, 23 | }, 24 | }, 25 | extraLargeButton: { 26 | paddingTop: theme.spacing(1.5), 27 | paddingBottom: theme.spacing(1.5), 28 | [theme.breakpoints.up("xs")]: { 29 | paddingTop: theme.spacing(1), 30 | paddingBottom: theme.spacing(1), 31 | }, 32 | [theme.breakpoints.up("lg")]: { 33 | paddingTop: theme.spacing(2), 34 | paddingBottom: theme.spacing(2), 35 | }, 36 | }, 37 | card: { 38 | boxShadow: theme.shadows[4], 39 | marginLeft: theme.spacing(2), 40 | marginRight: theme.spacing(2), 41 | [theme.breakpoints.up("xs")]: { 42 | paddingTop: theme.spacing(3), 43 | paddingBottom: theme.spacing(3), 44 | }, 45 | [theme.breakpoints.up("sm")]: { 46 | paddingTop: theme.spacing(5), 47 | paddingBottom: theme.spacing(5), 48 | paddingLeft: theme.spacing(4), 49 | paddingRight: theme.spacing(4), 50 | }, 51 | [theme.breakpoints.up("md")]: { 52 | paddingTop: theme.spacing(5.5), 53 | paddingBottom: theme.spacing(5.5), 54 | paddingLeft: theme.spacing(5), 55 | paddingRight: theme.spacing(5), 56 | }, 57 | [theme.breakpoints.up("lg")]: { 58 | paddingTop: theme.spacing(6), 59 | paddingBottom: theme.spacing(6), 60 | paddingLeft: theme.spacing(6), 61 | paddingRight: theme.spacing(6), 62 | }, 63 | [theme.breakpoints.down("lg")]: { 64 | width: "auto", 65 | }, 66 | }, 67 | wrapper: { 68 | position: "relative", 69 | backgroundColor: theme.palette.secondary.main, 70 | paddingBottom: theme.spacing(2), 71 | }, 72 | image: { 73 | maxWidth: "100%", 74 | verticalAlign: "middle", 75 | borderRadius: theme.shape.borderRadius, 76 | boxShadow: theme.shadows[4], 77 | }, 78 | container: { 79 | marginTop: theme.spacing(6), 80 | marginBottom: theme.spacing(12), 81 | [theme.breakpoints.down("md")]: { 82 | marginBottom: theme.spacing(9), 83 | }, 84 | [theme.breakpoints.down("sm")]: { 85 | marginBottom: theme.spacing(6), 86 | }, 87 | [theme.breakpoints.down("sm")]: { 88 | marginBottom: theme.spacing(3), 89 | }, 90 | }, 91 | containerFix: { 92 | [theme.breakpoints.up("md")]: { 93 | maxWidth: "none !important", 94 | }, 95 | }, 96 | waveBorder: { 97 | paddingTop: theme.spacing(4), 98 | }, 99 | }); 100 | 101 | function HeadSection(props) { 102 | const { classes, theme, width } = props; 103 | return ( 104 | 105 |
106 |
107 | 108 | 113 |
114 | 115 | 116 | 122 | 123 | 126 | Free Template for building an SaaS app using 127 | Material-UI 128 | 129 | 130 |
131 | 132 | 136 | Lorem ipsum dolor sit amet, consetetur sadipscing 137 | elitr, sed diam nonumy eirmod tempor invidunt 138 | 139 | 140 | 150 |
151 |
152 |
153 | 154 | 155 | 160 | 161 | 162 |
163 |
164 |
165 |
166 |
167 |
168 | 174 |
175 | ); 176 | } 177 | 178 | HeadSection.propTypes = { 179 | classes: PropTypes.object, 180 | width: PropTypes.string, 181 | theme: PropTypes.object, 182 | }; 183 | 184 | export default withWidth()( 185 | withStyles(styles, { withTheme: true })(HeadSection) 186 | ); 187 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 4 | import CheckIcon from "@material-ui/icons/Check"; 5 | 6 | const styles = theme => ({ 7 | card: { 8 | paddingTop: theme.spacing(6), 9 | paddingBottom: theme.spacing(6), 10 | paddingLeft: theme.spacing(4), 11 | paddingRight: theme.spacing(4), 12 | marginTop: theme.spacing(2), 13 | border: `3px solid ${theme.palette.primary.dark}`, 14 | borderRadius: theme.shape.borderRadius * 2 15 | }, 16 | cardHightlighted: { 17 | paddingTop: theme.spacing(8), 18 | paddingBottom: theme.spacing(4), 19 | paddingLeft: theme.spacing(4), 20 | paddingRight: theme.spacing(4), 21 | border: `3px solid ${theme.palette.primary.dark}`, 22 | borderRadius: theme.shape.borderRadius * 2, 23 | backgroundColor: theme.palette.primary.main, 24 | [theme.breakpoints.down("xs")]: { 25 | marginTop: theme.spacing(2) 26 | } 27 | }, 28 | title: { 29 | color: theme.palette.primary.main 30 | } 31 | }); 32 | 33 | function PriceCard(props) { 34 | const { classes, theme, title, pricing, features, highlighted } = props; 35 | return ( 36 |
37 | 38 | 42 | {title} 43 | 44 | 45 | 46 | 50 | {pricing} 51 | 52 | 53 | {features.map((feature, index) => ( 54 | 55 | 62 | 63 | 67 | {feature} 68 | 69 | 70 | 71 | ))} 72 |
73 | ); 74 | } 75 | 76 | PriceCard.propTypes = { 77 | classes: PropTypes.object.isRequired, 78 | theme: PropTypes.object.isRequired, 79 | title: PropTypes.string.isRequired, 80 | pricing: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired, 81 | highlighted: PropTypes.bool 82 | }; 83 | 84 | export default withStyles(styles, { withTheme: true })(PriceCard); 85 | -------------------------------------------------------------------------------- /src/logged_out/components/home/PricingSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { 5 | Grid, 6 | Typography, 7 | isWidthUp, 8 | withWidth, 9 | withStyles 10 | } from "@material-ui/core"; 11 | import PriceCard from "./PriceCard"; 12 | import calculateSpacing from "./calculateSpacing"; 13 | 14 | const styles = theme => ({ 15 | containerFix: { 16 | [theme.breakpoints.down("md")]: { 17 | paddingLeft: theme.spacing(6), 18 | paddingRight: theme.spacing(6) 19 | }, 20 | [theme.breakpoints.down("sm")]: { 21 | paddingLeft: theme.spacing(4), 22 | paddingRight: theme.spacing(4) 23 | }, 24 | [theme.breakpoints.down("xs")]: { 25 | paddingLeft: theme.spacing(2), 26 | paddingRight: theme.spacing(2) 27 | }, 28 | overflow: "hidden", 29 | paddingTop: theme.spacing(1), 30 | paddingBottom: theme.spacing(1) 31 | }, 32 | cardWrapper: { 33 | [theme.breakpoints.down("xs")]: { 34 | marginLeft: "auto", 35 | marginRight: "auto", 36 | maxWidth: 340 37 | } 38 | }, 39 | cardWrapperHighlighted: { 40 | [theme.breakpoints.down("xs")]: { 41 | marginLeft: "auto", 42 | marginRight: "auto", 43 | maxWidth: 360 44 | } 45 | } 46 | }); 47 | 48 | function PricingSection(props) { 49 | const { width, classes } = props; 50 | return ( 51 |
52 | 53 | Pricing 54 | 55 |
56 | 61 | 69 | 73 | $14.99 74 | / month 75 | 76 | } 77 | features={["Feature 1", "Feature 2", "Feature 3"]} 78 | /> 79 | 80 | 89 | 94 | $29.99 95 | / month 96 | 97 | } 98 | features={["Feature 1", "Feature 2", "Feature 3"]} 99 | /> 100 | 101 | 110 | 114 | $49.99 115 | / month 116 | 117 | } 118 | features={["Feature 1", "Feature 2", "Feature 3"]} 119 | /> 120 | 121 | 130 | 134 | $99.99 135 | / month 136 | 137 | } 138 | features={["Feature 1", "Feature 2", "Feature 3"]} 139 | /> 140 | 141 | 142 |
143 |
144 | ); 145 | } 146 | 147 | PricingSection.propTypes = { 148 | width: PropTypes.string.isRequired 149 | }; 150 | 151 | export default withStyles(styles, { withTheme: true })( 152 | withWidth()(PricingSection) 153 | ); 154 | -------------------------------------------------------------------------------- /src/logged_out/components/home/calculateSpacing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This calculates the spacing for the 3 | * grid container component based on the viewsize 4 | */ 5 | 6 | import { isWidthUp } from "@material-ui/core/withWidth"; 7 | 8 | function calculateSpacing(width) { 9 | if (isWidthUp("lg", width)) { 10 | return 5; 11 | } 12 | if (isWidthUp("md", width)) { 13 | return 4; 14 | } 15 | if (isWidthUp("sm", width)) { 16 | return 3; 17 | } 18 | return 2; 19 | } 20 | 21 | export default calculateSpacing; 22 | -------------------------------------------------------------------------------- /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 { 5 | AppBar, 6 | Toolbar, 7 | Typography, 8 | Button, 9 | Hidden, 10 | IconButton, 11 | withStyles 12 | } from "@material-ui/core"; 13 | import MenuIcon from "@material-ui/icons/Menu"; 14 | import HomeIcon from "@material-ui/icons/Home"; 15 | import HowToRegIcon from "@material-ui/icons/HowToReg"; 16 | import LockOpenIcon from "@material-ui/icons/LockOpen"; 17 | import BookIcon from "@material-ui/icons/Book"; 18 | import NavigationDrawer from "../../../shared/components/NavigationDrawer"; 19 | 20 | const styles = theme => ({ 21 | appBar: { 22 | boxShadow: theme.shadows[6], 23 | backgroundColor: theme.palette.common.white 24 | }, 25 | toolbar: { 26 | display: "flex", 27 | justifyContent: "space-between" 28 | }, 29 | menuButtonText: { 30 | fontSize: theme.typography.body1.fontSize, 31 | fontWeight: theme.typography.h6.fontWeight 32 | }, 33 | brandText: { 34 | fontFamily: "'Baloo Bhaijaan', cursive", 35 | fontWeight: 400 36 | }, 37 | noDecoration: { 38 | textDecoration: "none !important" 39 | } 40 | }); 41 | 42 | function NavBar(props) { 43 | const { 44 | classes, 45 | openRegisterDialog, 46 | openLoginDialog, 47 | handleMobileDrawerOpen, 48 | handleMobileDrawerClose, 49 | mobileDrawerOpen, 50 | selectedTab 51 | } = props; 52 | const menuItems = [ 53 | { 54 | link: "/", 55 | name: "Home", 56 | icon: 57 | }, 58 | { 59 | link: "/blog", 60 | name: "Blog", 61 | icon: 62 | }, 63 | { 64 | name: "Register", 65 | onClick: openRegisterDialog, 66 | icon: 67 | }, 68 | { 69 | name: "Login", 70 | onClick: openLoginDialog, 71 | icon: 72 | } 73 | ]; 74 | return ( 75 |
76 | 77 | 78 |
79 | 85 | Wa 86 | 87 | 93 | Ver 94 | 95 |
96 |
97 | 98 | 103 | 104 | 105 | 106 | 107 | {menuItems.map(element => { 108 | if (element.link) { 109 | return ( 110 | 116 | 123 | 124 | ); 125 | } 126 | return ( 127 | 136 | ); 137 | })} 138 | 139 |
140 |
141 |
142 | 149 |
150 | ); 151 | } 152 | 153 | NavBar.propTypes = { 154 | classes: PropTypes.object.isRequired, 155 | handleMobileDrawerOpen: PropTypes.func, 156 | handleMobileDrawerClose: PropTypes.func, 157 | mobileDrawerOpen: PropTypes.bool, 158 | selectedTab: PropTypes.string, 159 | openRegisterDialog: PropTypes.func.isRequired, 160 | openLoginDialog: PropTypes.func.isRequired 161 | }; 162 | 163 | export default withStyles(styles, { withTheme: true })(memo(NavBar)); 164 | -------------------------------------------------------------------------------- /src/logged_out/components/register_login/ChangePasswordDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | TextField, 5 | Dialog, 6 | DialogContent, 7 | DialogActions, 8 | Button, 9 | Typography, 10 | withStyles, 11 | } from "@material-ui/core"; 12 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 13 | 14 | const styles = (theme) => ({ 15 | dialogContent: { 16 | paddingTop: theme.spacing(2), 17 | }, 18 | dialogActions: { 19 | paddingTop: theme.spacing(2), 20 | paddingBottom: theme.spacing(2), 21 | paddingRight: theme.spacing(2), 22 | }, 23 | }); 24 | 25 | function ChangePassword(props) { 26 | const { onClose, classes, setLoginStatus } = props; 27 | const [isLoading, setIsLoading] = useState(false); 28 | 29 | const sendPasswordEmail = useCallback(() => { 30 | setIsLoading(true); 31 | setTimeout(() => { 32 | setLoginStatus("verificationEmailSend"); 33 | setIsLoading(false); 34 | onClose(); 35 | }, 1500); 36 | }, [setIsLoading, setLoginStatus, onClose]); 37 | 38 | return ( 39 | 47 |
{ 49 | e.preventDefault(); 50 | sendPasswordEmail(); 51 | }} 52 | > 53 | 54 | 55 | Enter your email address below and we will send you instructions on 56 | how to reset your password. 57 | 58 | 68 | 69 | 70 | 73 | 82 | 83 |
84 |
85 | ); 86 | } 87 | 88 | ChangePassword.propTypes = { 89 | onClose: PropTypes.func.isRequired, 90 | classes: PropTypes.object.isRequired, 91 | theme: PropTypes.object.isRequired, 92 | setLoginStatus: PropTypes.func.isRequired, 93 | }; 94 | 95 | export default withStyles(styles, { withTheme: true })(ChangePassword); 96 | -------------------------------------------------------------------------------- /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 { 6 | TextField, 7 | Button, 8 | Checkbox, 9 | Typography, 10 | FormControlLabel, 11 | withStyles, 12 | } from "@material-ui/core"; 13 | import FormDialog from "../../../shared/components/FormDialog"; 14 | import HighlightedInformation from "../../../shared/components/HighlightedInformation"; 15 | import ButtonCircularProgress from "../../../shared/components/ButtonCircularProgress"; 16 | import VisibilityPasswordTextField from "../../../shared/components/VisibilityPasswordTextField"; 17 | 18 | const styles = (theme) => ({ 19 | forgotPassword: { 20 | marginTop: theme.spacing(2), 21 | color: theme.palette.primary.main, 22 | cursor: "pointer", 23 | "&:enabled:hover": { 24 | color: theme.palette.primary.dark, 25 | }, 26 | "&:enabled:focus": { 27 | color: theme.palette.primary.dark, 28 | }, 29 | }, 30 | disabledText: { 31 | cursor: "auto", 32 | color: theme.palette.text.disabled, 33 | }, 34 | formControlLabel: { 35 | marginRight: 0, 36 | }, 37 | }); 38 | 39 | function LoginDialog(props) { 40 | const { 41 | setStatus, 42 | history, 43 | classes, 44 | onClose, 45 | openChangePasswordDialog, 46 | status, 47 | } = props; 48 | const [isLoading, setIsLoading] = useState(false); 49 | const [isPasswordVisible, setIsPasswordVisible] = useState(false); 50 | const loginEmail = useRef(); 51 | const loginPassword = useRef(); 52 | 53 | const login = useCallback(() => { 54 | setIsLoading(true); 55 | setStatus(null); 56 | if (loginEmail.current.value !== "test@web.com") { 57 | setTimeout(() => { 58 | setStatus("invalidEmail"); 59 | setIsLoading(false); 60 | }, 1500); 61 | } else if (loginPassword.current.value !== "HaRzwc") { 62 | setTimeout(() => { 63 | setStatus("invalidPassword"); 64 | setIsLoading(false); 65 | }, 1500); 66 | } else { 67 | setTimeout(() => { 68 | history.push("/c/dashboard"); 69 | }, 150); 70 | } 71 | }, [setIsLoading, loginEmail, loginPassword, history, setStatus]); 72 | 73 | return ( 74 | 75 | { 80 | e.preventDefault(); 81 | login(); 82 | }} 83 | hideBackdrop 84 | headline="Login" 85 | content={ 86 | 87 | { 99 | if (status === "invalidEmail") { 100 | setStatus(null); 101 | } 102 | }} 103 | helperText={ 104 | status === "invalidEmail" && 105 | "This email address isn't associated with an account." 106 | } 107 | FormHelperTextProps={{ error: true }} 108 | /> 109 | { 119 | if (status === "invalidPassword") { 120 | setStatus(null); 121 | } 122 | }} 123 | helperText={ 124 | status === "invalidPassword" ? ( 125 | 126 | Incorrect password. Try again, or click on{" "} 127 | "Forgot Password?" to reset it. 128 | 129 | ) : ( 130 | "" 131 | ) 132 | } 133 | FormHelperTextProps={{ error: true }} 134 | onVisibilityChange={setIsPasswordVisible} 135 | isVisible={isPasswordVisible} 136 | /> 137 | } 140 | label={Remember me} 141 | /> 142 | {status === "verificationEmailSend" ? ( 143 | 144 | We have send instructions on how to reset your password to your 145 | email address 146 | 147 | ) : ( 148 | 149 | Email is: test@web.com 150 |
151 | Password is: HaRzwc 152 |
153 | )} 154 |
155 | } 156 | actions={ 157 | 158 | 169 | { 180 | // For screenreaders listen to space and enter events 181 | if ( 182 | (!isLoading && event.keyCode === 13) || 183 | event.keyCode === 32 184 | ) { 185 | openChangePasswordDialog(); 186 | } 187 | }} 188 | > 189 | Forgot Password? 190 | 191 | 192 | } 193 | /> 194 |
195 | ); 196 | } 197 | 198 | LoginDialog.propTypes = { 199 | classes: PropTypes.object.isRequired, 200 | onClose: PropTypes.func.isRequired, 201 | setStatus: PropTypes.func.isRequired, 202 | openChangePasswordDialog: PropTypes.func.isRequired, 203 | history: PropTypes.object.isRequired, 204 | status: PropTypes.string, 205 | }; 206 | 207 | export default withRouter(withStyles(styles)(LoginDialog)); 208 | -------------------------------------------------------------------------------- /src/logged_out/dummy_data/blogPosts.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Typography } from "@material-ui/core"; 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 | export default [ 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 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // This optional code is used to register a service worker. 4 | // register() is not called by default. 5 | 6 | // This lets the app load faster on subsequent visits in production, and gives 7 | // it offline capabilities. However, it also means that developers (and users) 8 | // will only see deployed updates on subsequent visits to a page, after all the 9 | // existing tabs open on the page have been closed, since previously cached 10 | // resources are updated in the background. 11 | 12 | // To learn more about the benefits of this model and instructions on how to 13 | // opt-in, read http://bit.ly/CRA-PWA 14 | 15 | const isLocalhost = Boolean( 16 | window.location.hostname === "localhost" || 17 | // [::1] is the IPv6 localhost address. 18 | window.location.hostname === "[::1]" || 19 | // 127.0.0.1/8 is considered localhost for IPv4. 20 | window.location.hostname.match( 21 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 22 | ) 23 | ); 24 | 25 | export function register(config) { 26 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 27 | // The URL constructor is available in all browsers that support SW. 28 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener("load", () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | if (isLocalhost) { 39 | // This is running on localhost. Let's check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl, config); 41 | 42 | // Add some additional logging to localhost, pointing developers to the 43 | // service worker/PWA documentation. 44 | navigator.serviceWorker.ready.then(() => { 45 | console.log( 46 | "This web app is being served cache-first by a service " + 47 | "worker. To learn more, visit http://bit.ly/CRA-PWA" 48 | ); 49 | }); 50 | } else { 51 | // Is not localhost. Just register service worker 52 | registerValidSW(swUrl, config); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | function registerValidSW(swUrl, config) { 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then(registration => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | if (installingWorker == null) { 65 | return; 66 | } 67 | installingWorker.onstatechange = () => { 68 | if (installingWorker.state === "installed") { 69 | if (navigator.serviceWorker.controller) { 70 | // At this point, the updated precached content has been fetched, 71 | // but the previous service worker will still serve the older 72 | // content until all client tabs are closed. 73 | console.log( 74 | "New content is available and will be used when all " + 75 | "tabs for this page are closed. See http://bit.ly/CRA-PWA." 76 | ); 77 | 78 | // Execute callback 79 | if (config && config.onUpdate) { 80 | config.onUpdate(registration); 81 | } 82 | } else { 83 | // At this point, everything has been precached. 84 | // It's the perfect time to display a 85 | // "Content is cached for offline use." message. 86 | console.log("Content is cached for offline use."); 87 | 88 | // Execute callback 89 | if (config && config.onSuccess) { 90 | config.onSuccess(registration); 91 | } 92 | } 93 | } 94 | }; 95 | }; 96 | }) 97 | .catch(error => { 98 | console.error("Error during service worker registration:", error); 99 | }); 100 | } 101 | 102 | function checkValidServiceWorker(swUrl, config) { 103 | // Check if the service worker can be found. If it can't reload the page. 104 | fetch(swUrl) 105 | .then(response => { 106 | // Ensure service worker exists, and that we really are getting a JS file. 107 | const contentType = response.headers.get("content-type"); 108 | if ( 109 | response.status === 404 || 110 | (contentType != null && contentType.indexOf("javascript") === -1) 111 | ) { 112 | // No service worker found. Probably a different app. Reload the page. 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister().then(() => { 115 | window.location.reload(); 116 | }); 117 | }); 118 | } else { 119 | // Service worker found. Proceed as normal. 120 | registerValidSW(swUrl, config); 121 | } 122 | }) 123 | .catch(() => { 124 | console.log( 125 | "No internet connection found. App is running in offline mode." 126 | ); 127 | }); 128 | } 129 | 130 | export function unregister() { 131 | if ("serviceWorker" in navigator) { 132 | navigator.serviceWorker.ready.then(registration => { 133 | registration.unregister(); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /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 { 4 | Paper, 5 | DialogTitle, 6 | DialogContent, 7 | DialogActions, 8 | Box, 9 | withStyles 10 | } from "@material-ui/core"; 11 | 12 | const styles = theme => ({ 13 | helpPadding: { 14 | "@media (max-width: 400px)": { 15 | paddingLeft: theme.spacing(1), 16 | paddingRight: theme.spacing(1) 17 | } 18 | }, 19 | fullWidth: { 20 | width: "100%" 21 | } 22 | }); 23 | 24 | function ActionPaper(props) { 25 | const { 26 | theme, 27 | classes, 28 | title, 29 | content, 30 | maxWidth, 31 | actions, 32 | helpPadding, 33 | fullWidthActions 34 | } = props; 35 | return ( 36 | 37 | 38 | {title && {title}} 39 | {content && ( 40 | 43 | {content} 44 | 45 | )} 46 | {actions && ( 47 | 48 | 51 | {actions} 52 | 53 | 54 | )} 55 | 56 | 57 | ); 58 | } 59 | 60 | ActionPaper.propTypes = { 61 | theme: PropTypes.object.isRequired, 62 | classes: PropTypes.object.isRequired, 63 | title: PropTypes.oneOfType([ 64 | PropTypes.element, 65 | PropTypes.func, 66 | PropTypes.string 67 | ]), 68 | content: PropTypes.element, 69 | maxWidth: PropTypes.string, 70 | actions: PropTypes.element, 71 | helpPadding: PropTypes.bool, 72 | fullWidthActions: PropTypes.bool 73 | }; 74 | 75 | export default withStyles(styles, { withTheme: true })(ActionPaper); 76 | -------------------------------------------------------------------------------- /src/shared/components/Bordered.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core"; 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, withStyles } from "@material-ui/core"; 4 | 5 | const styles = theme => ({ 6 | circularProgress: { 7 | color: theme.palette.secondary.main 8 | } 9 | }); 10 | 11 | function ButtonCircularProgress(props) { 12 | const { size, classes } = props; 13 | return ( 14 | 15 | 20 | 21 | ); 22 | } 23 | 24 | ButtonCircularProgress.propTypes = { 25 | size: PropTypes.number, 26 | classes: PropTypes.object.isRequired 27 | }; 28 | 29 | export default withStyles(styles, { withTheme: true })(ButtonCircularProgress); 30 | -------------------------------------------------------------------------------- /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 { 13 | Card, 14 | CardContent, 15 | Typography, 16 | IconButton, 17 | Menu, 18 | MenuItem, 19 | withStyles, 20 | Box, 21 | } from "@material-ui/core"; 22 | import MoreVertIcon from "@material-ui/icons/MoreVert"; 23 | 24 | const styles = (theme) => ({ 25 | cardContentInner: { 26 | marginTop: theme.spacing(-4), 27 | }, 28 | }); 29 | 30 | function labelFormatter(label) { 31 | return format(new Date(label * 1000), "MMMM d, p yyyy"); 32 | } 33 | 34 | function calculateMin(data, yKey, factor) { 35 | let max = Number.POSITIVE_INFINITY; 36 | data.forEach((element) => { 37 | if (max > element[yKey]) { 38 | max = element[yKey]; 39 | } 40 | }); 41 | return Math.round(max - max * factor); 42 | } 43 | 44 | const itemHeight = 216; 45 | const options = ["1 Week", "1 Month", "6 Months"]; 46 | 47 | function CardChart(props) { 48 | const { color, data, title, classes, theme, height } = props; 49 | const [anchorEl, setAnchorEl] = useState(null); 50 | const [selectedOption, setSelectedOption] = useState("1 Month"); 51 | 52 | const handleClick = useCallback( 53 | (event) => { 54 | setAnchorEl(event.currentTarget); 55 | }, 56 | [setAnchorEl] 57 | ); 58 | 59 | const formatter = useCallback( 60 | (value) => { 61 | return [value, title]; 62 | }, 63 | [title] 64 | ); 65 | 66 | const getSubtitle = useCallback(() => { 67 | switch (selectedOption) { 68 | case "1 Week": 69 | return "Last week"; 70 | case "1 Month": 71 | return "Last month"; 72 | case "6 Months": 73 | return "Last 6 months"; 74 | default: 75 | throw new Error("No branch selected in switch-statement"); 76 | } 77 | }, [selectedOption]); 78 | 79 | const processData = useCallback(() => { 80 | let seconds; 81 | switch (selectedOption) { 82 | case "1 Week": 83 | seconds = 60 * 60 * 24 * 7; 84 | break; 85 | case "1 Month": 86 | seconds = 60 * 60 * 24 * 31; 87 | break; 88 | case "6 Months": 89 | seconds = 60 * 60 * 24 * 31 * 6; 90 | break; 91 | default: 92 | throw new Error("No branch selected in switch-statement"); 93 | } 94 | const minSeconds = new Date() / 1000 - seconds; 95 | const arr = []; 96 | for (let i = 0; i < data.length; i += 1) { 97 | if (minSeconds < data[i].timestamp) { 98 | arr.unshift(data[i]); 99 | } 100 | } 101 | return arr; 102 | }, [data, selectedOption]); 103 | 104 | const handleClose = useCallback(() => { 105 | setAnchorEl(null); 106 | }, [setAnchorEl]); 107 | 108 | const selectOption = useCallback( 109 | (selectedOption) => { 110 | setSelectedOption(selectedOption); 111 | handleClose(); 112 | }, 113 | [setSelectedOption, handleClose] 114 | ); 115 | 116 | const isOpen = Boolean(anchorEl); 117 | return ( 118 | 119 | 120 | 121 |
122 | {title} 123 | 124 | {getSubtitle()} 125 | 126 |
127 |
128 | 134 | 135 | 136 | 149 | {options.map((option) => ( 150 | { 154 | selectOption(option); 155 | }} 156 | name={option} 157 | > 158 | {option} 159 | 160 | ))} 161 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 | 175 | 179 | 185 | 204 | 205 | 206 | 207 | 208 |
209 | ); 210 | } 211 | 212 | CardChart.propTypes = { 213 | color: PropTypes.string.isRequired, 214 | data: PropTypes.array.isRequired, 215 | title: PropTypes.string.isRequired, 216 | classes: PropTypes.object.isRequired, 217 | theme: PropTypes.object.isRequired, 218 | height: PropTypes.string.isRequired, 219 | }; 220 | 221 | export default withStyles(styles, { withTheme: true })(CardChart); 222 | -------------------------------------------------------------------------------- /src/shared/components/ColoredButton.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Button, createMuiTheme, MuiThemeProvider } from "@material-ui/core"; 4 | 5 | function ColoredButton(props) { 6 | const { color, children, theme } = props; 7 | const buttonTheme = createMuiTheme({ 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 | 21 | 22 | ); 23 | } 24 | 25 | ColoredButton.propTypes = { 26 | color: PropTypes.string.isRequired 27 | }; 28 | 29 | export default memo(ColoredButton); 30 | -------------------------------------------------------------------------------- /src/shared/components/ColorfulChip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Chip } from "@material-ui/core"; 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 "@material-ui/core"; 11 | import ButtonCircularProgress from "./ButtonCircularProgress"; 12 | 13 | function ConfirmationDialog(props) { 14 | const { open, onClose, loading, title, content, onConfirm } = props; 15 | return ( 16 | 22 | {title} 23 | 24 | {content} 25 | 26 | 27 | 30 | 38 | 39 | 40 | ); 41 | } 42 | 43 | ConfirmationDialog.propTypes = { 44 | open: PropTypes.bool, 45 | onClose: PropTypes.func, 46 | loading: PropTypes.bool, 47 | title: PropTypes.string, 48 | content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 49 | onConfirm: PropTypes.func 50 | }; 51 | 52 | export default ConfirmationDialog; 53 | -------------------------------------------------------------------------------- /src/shared/components/ConsecutiveSnackbarMessages.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useRef, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Snackbar, withStyles } from "@material-ui/core"; 4 | 5 | const styles = (theme) => ({ 6 | root: { 7 | backgroundColor: theme.palette.primary.main, 8 | paddingTop: 0, 9 | paddingBottom: 0, 10 | }, 11 | }); 12 | 13 | function ConsecutiveSnackbars(props) { 14 | const { classes, getPushMessageFromChild } = props; 15 | const [isOpen, setIsOpen] = useState(false); 16 | const [messageInfo, setMessageInfo] = useState({}); 17 | const queue = useRef([]); 18 | 19 | const processQueue = useCallback(() => { 20 | if (queue.current.length > 0) { 21 | setMessageInfo(queue.current.shift()); 22 | setIsOpen(true); 23 | } 24 | }, [setMessageInfo, setIsOpen, queue]); 25 | 26 | const handleClose = useCallback((_, reason) => { 27 | if (reason === "clickaway") { 28 | return; 29 | } 30 | setIsOpen(false); 31 | }, [setIsOpen]); 32 | 33 | const pushMessage = useCallback(message => { 34 | queue.current.push({ 35 | message, 36 | key: new Date().getTime(), 37 | }); 38 | if (isOpen) { 39 | // immediately begin dismissing current message 40 | // to start showing new one 41 | setIsOpen(false); 42 | } else { 43 | processQueue(); 44 | } 45 | }, [queue, isOpen, setIsOpen, processQueue]); 46 | 47 | useEffect(() => { 48 | getPushMessageFromChild(pushMessage); 49 | }, [getPushMessageFromChild, pushMessage]); 50 | 51 | return ( 52 | {messageInfo.message ? messageInfo.message.text : null} 70 | } 71 | /> 72 | ); 73 | 74 | } 75 | 76 | ConsecutiveSnackbars.propTypes = { 77 | getPushMessageFromChild: PropTypes.func.isRequired, 78 | classes: PropTypes.object.isRequired, 79 | }; 80 | 81 | export default withStyles(styles, { withTheme: true })(ConsecutiveSnackbars); 82 | -------------------------------------------------------------------------------- /src/shared/components/DateTimePicker.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | MuiPickersUtilsProvider, 5 | DateTimePicker as DTPicker 6 | } from "@material-ui/pickers"; 7 | import DateFnsUtils from "@date-io/date-fns"; 8 | import { withTheme, MuiThemeProvider, createMuiTheme } from "@material-ui/core"; 9 | import AccessTime from "@material-ui/icons/AccessTime"; 10 | import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; 11 | import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; 12 | import DateRange from "@material-ui/icons/DateRange"; 13 | 14 | const Theme2 = theme => 15 | createMuiTheme({ 16 | ...theme, 17 | overrides: { 18 | MuiOutlinedInput: { 19 | root: { 20 | width: 190, 21 | "@media (max-width: 400px)": { 22 | width: 160 23 | }, 24 | "@media (max-width: 360px)": { 25 | width: 140 26 | }, 27 | "@media (max-width: 340px)": { 28 | width: 120 29 | } 30 | }, 31 | input: { 32 | padding: "9px 14.5px" 33 | } 34 | } 35 | } 36 | }); 37 | 38 | function DateTimePicker(props) { 39 | const { disabled, value, onChange } = props; 40 | return ( 41 | 42 | 43 | } 46 | rightArrowIcon={} 47 | timeIcon={} 48 | dateRangeIcon={} 49 | variant="outlined" 50 | disabled={disabled} 51 | value={value} 52 | onChange={onChange} 53 | {...props} 54 | inputProps={{ style: { width: "100%", cursor: "pointer" } }} 55 | /> 56 | 57 | 58 | ); 59 | } 60 | 61 | DateTimePicker.propTypes = { 62 | disabled: PropTypes.bool, 63 | value: PropTypes.instanceOf(Date), 64 | onChange: PropTypes.func 65 | }; 66 | 67 | export default withTheme(DateTimePicker); 68 | -------------------------------------------------------------------------------- /src/shared/components/DialogTitleWithCloseIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | IconButton, 5 | DialogTitle, 6 | Typography, 7 | Box, 8 | withTheme 9 | } from "@material-ui/core"; 10 | import CloseIcon from "@material-ui/icons/Close"; 11 | 12 | function DialogTitleWithCloseIcon(props) { 13 | const { 14 | theme, 15 | paddingBottom, 16 | onClose, 17 | disabled, 18 | title, 19 | disablePadding 20 | } = props; 21 | return ( 22 | 36 | 37 | {title} 38 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | 51 | DialogTitleWithCloseIcon.propTypes = { 52 | theme: PropTypes.object, 53 | paddingBottom: PropTypes.number, 54 | onClose: PropTypes.func, 55 | disabled: PropTypes.bool, 56 | title: PropTypes.string, 57 | disablePadding: PropTypes.bool 58 | }; 59 | 60 | export default withTheme(DialogTitleWithCloseIcon); 61 | -------------------------------------------------------------------------------- /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, withStyles } from "@material-ui/core"; 6 | import ColoredButton from "./ColoredButton"; 7 | 8 | const styles = { 9 | button: { 10 | borderWidth: 1, 11 | borderColor: "rgba(0, 0, 0, 0.23)", 12 | borderTopLeftRadius: 0, 13 | borderBottomLeftRadius: 0 14 | }, 15 | fullHeight: { 16 | height: "100%" 17 | } 18 | }; 19 | 20 | function getColor(isDragAccept, isDragReject, theme) { 21 | if (isDragAccept) { 22 | return theme.palette.success.main; 23 | } 24 | if (isDragReject) { 25 | return theme.palette.error.dark; 26 | } 27 | return theme.palette.common.black; 28 | } 29 | 30 | function Dropzone(props) { 31 | const { onDrop, accept, fullHeight, children, classes, style, theme } = props; 32 | const { 33 | getRootProps, 34 | getInputProps, 35 | isDragAccept, 36 | isDragReject 37 | } = useDropzone({ 38 | accept: accept, 39 | onDrop: onDrop 40 | }); 41 | return ( 42 | 43 | 44 | 54 | {children} 55 | 56 | 57 | ); 58 | } 59 | 60 | Dropzone.propTypes = { 61 | classes: PropTypes.object.isRequired, 62 | theme: PropTypes.object.isRequired, 63 | onDrop: PropTypes.func, 64 | accept: PropTypes.string, 65 | fullHeight: PropTypes.bool, 66 | style: PropTypes.object, 67 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) 68 | }; 69 | 70 | export default withStyles(styles, { withTheme: true })(Dropzone); 71 | -------------------------------------------------------------------------------- /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 { 6 | TextField, 7 | IconButton, 8 | Collapse, 9 | FormHelperText, 10 | Box, 11 | Grid, 12 | withStyles 13 | } from "@material-ui/core"; 14 | import EmojiEmotionsIcon from "@material-ui/icons/EmojiEmotions"; 15 | import CloseIcon from "@material-ui/icons/Close"; 16 | import countWithEmojis from "../functions/countWithEmojis"; 17 | 18 | const styles = theme => ({ 19 | "@global": { 20 | ".emoji-mart-category-label": theme.typography.body1, 21 | ".emoji-mart-bar": { display: "none !important" }, 22 | ".emoji-mart-search input": { 23 | ...theme.typography.body1, 24 | ...theme.border 25 | }, 26 | ".emoji-mart-search": { 27 | marginTop: `${theme.spacing(1)}px !important`, 28 | paddingRight: `${theme.spacing(1)}px !important`, 29 | paddingLeft: `${theme.spacing(1)}px !important`, 30 | paddingBottom: `${theme.spacing(1)}px !important` 31 | }, 32 | ".emoji-mart-search-icon": { 33 | top: "5px !important", 34 | right: "14px !important", 35 | fontSize: 20 36 | }, 37 | ".emoji-mart-scroll": { 38 | height: 240 39 | }, 40 | ".emoji-mart": { 41 | ...theme.border 42 | } 43 | }, 44 | floatButtonWrapper: { 45 | position: "absolute", 46 | bottom: 12, 47 | right: 12 48 | }, 49 | floatButtonSVG: { 50 | color: theme.palette.primary.light 51 | }, 52 | relative: { 53 | position: "relative" 54 | } 55 | }); 56 | 57 | /** 58 | * Emojis whose unified is greater than 5 sometimes 59 | * are not displayed correcty in the browser. 60 | * We won't display them. 61 | */ 62 | const emojisToShowFilter = emoji => { 63 | if (emoji.unified.length > 5) { 64 | return false; 65 | } 66 | return true; 67 | }; 68 | 69 | function EmojiTextarea(props) { 70 | const { 71 | theme, 72 | classes, 73 | rightContent, 74 | placeholder, 75 | maxCharacters, 76 | emojiSet, 77 | inputClassName, 78 | onChange 79 | } = props; 80 | const [open, setOpen] = useState(false); 81 | const [value, setValue] = useState(""); 82 | const [characters, setCharacters] = useState(0); 83 | 84 | const onSelectEmoji = useCallback( 85 | emoji => { 86 | let _characters; 87 | let _value = value + emoji.native; 88 | if (maxCharacters) { 89 | _characters = countWithEmojis(_value); 90 | if (_characters > maxCharacters) { 91 | return; 92 | } 93 | } 94 | if (onChange) { 95 | onChange(_value, _characters); 96 | } 97 | setValue(_value); 98 | setCharacters(_characters); 99 | }, 100 | [value, setValue, setCharacters, maxCharacters, onChange] 101 | ); 102 | 103 | const handleTextFieldChange = useCallback( 104 | event => { 105 | const { target } = event; 106 | const { value } = target; 107 | let characters; 108 | if (maxCharacters) { 109 | characters = countWithEmojis(value); 110 | if (characters > maxCharacters) { 111 | return; 112 | } 113 | } 114 | if (onChange) { 115 | onChange(value, characters); 116 | } 117 | setValue(value); 118 | setCharacters(characters); 119 | }, 120 | [maxCharacters, onChange, setValue, setCharacters] 121 | ); 122 | 123 | const toggleOpen = useCallback(() => { 124 | setOpen(!open); 125 | }, [open, setOpen]); 126 | 127 | return ( 128 | 129 | 130 | 137 | 151 |
152 | 153 | {open ? ( 154 | 155 | ) : ( 156 | 157 | )} 158 | 159 |
160 |
161 | {rightContent && ( 162 | 163 | {rightContent} 164 | 165 | )} 166 |
167 | {maxCharacters && ( 168 | = maxCharacters}> 169 | {`${characters}/${maxCharacters} characters`} 170 | 171 | )} 172 | 173 | 174 | 181 | 182 | 183 |
184 | ); 185 | } 186 | 187 | EmojiTextarea.propTypes = { 188 | theme: PropTypes.object.isRequired, 189 | classes: PropTypes.object.isRequired, 190 | emojiSet: PropTypes.string.isRequired, 191 | rightContent: PropTypes.element, 192 | placeholder: PropTypes.string, 193 | maxCharacters: PropTypes.number, 194 | onChange: PropTypes.func, 195 | inputClassName: PropTypes.string 196 | }; 197 | 198 | export default withStyles(styles, { withTheme: true })(EmojiTextarea); 199 | -------------------------------------------------------------------------------- /src/shared/components/EnhancedTableHead.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { 5 | Typography, 6 | TableCell, 7 | TableHead, 8 | TableRow, 9 | TableSortLabel, 10 | Tooltip, 11 | withStyles 12 | } from "@material-ui/core"; 13 | 14 | const styles = theme => ({ 15 | tableSortLabel: { 16 | cursor: "text", 17 | userSelect: "auto", 18 | color: "inherit !important" 19 | }, 20 | noIcon: { 21 | "& path": { 22 | display: "none !important" 23 | } 24 | }, 25 | paddingFix: { 26 | paddingLeft: theme.spacing(3) 27 | } 28 | }); 29 | 30 | function EnhancedTableHead(props) { 31 | const { order, orderBy, rows, onRequestSort, classes } = props; 32 | 33 | const createSortHandler = useCallback( 34 | property => event => { 35 | onRequestSort(event, property); 36 | }, 37 | [onRequestSort] 38 | ); 39 | 40 | return ( 41 | 42 | 43 | {rows.map((row, index) => ( 44 | 51 | {onRequestSort ? ( 52 | 57 | 62 | {row.label} 63 | 64 | 65 | ) : ( 66 | 69 | 70 | {row.label} 71 | 72 | 73 | )} 74 | 75 | ))} 76 | 77 | 78 | ); 79 | } 80 | EnhancedTableHead.propTypes = { 81 | classes: PropTypes.object.isRequired, 82 | theme: PropTypes.object.isRequired, 83 | onRequestSort: PropTypes.func, 84 | order: PropTypes.string, 85 | orderBy: PropTypes.string, 86 | rows: PropTypes.arrayOf(PropTypes.object).isRequired 87 | }; 88 | 89 | export default withStyles(styles, { withTheme: true })(EnhancedTableHead); 90 | -------------------------------------------------------------------------------- /src/shared/components/FormDialog.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Dialog, DialogContent, Box, withStyles } from "@material-ui/core"; 4 | import DialogTitleWithCloseIcon from "./DialogTitleWithCloseIcon"; 5 | 6 | const styles = theme => ({ 7 | dialogPaper: { 8 | display: "flex", 9 | flexDirection: "column", 10 | alignItems: "center", 11 | paddingBottom: theme.spacing(3), 12 | maxWidth: 420 13 | }, 14 | actions: { 15 | marginTop: theme.spacing(2) 16 | }, 17 | dialogPaperScrollPaper: { 18 | maxHeight: "none" 19 | }, 20 | dialogContent: { 21 | paddingTop: 0, 22 | paddingBottom: 0 23 | } 24 | }); 25 | 26 | /** 27 | * A Wrapper around the Dialog component to create centered 28 | * Login, Register or other Dialogs. 29 | */ 30 | function FormDialog(props) { 31 | const { 32 | classes, 33 | open, 34 | onClose, 35 | loading, 36 | headline, 37 | onFormSubmit, 38 | content, 39 | actions, 40 | hideBackdrop 41 | } = props; 42 | return ( 43 | 54 | 59 | 60 |
61 |
{content}
62 | 63 | {actions} 64 | 65 |
66 |
67 |
68 | ); 69 | } 70 | 71 | FormDialog.propTypes = { 72 | classes: PropTypes.object.isRequired, 73 | open: PropTypes.bool.isRequired, 74 | onClose: PropTypes.func.isRequired, 75 | headline: PropTypes.string.isRequired, 76 | loading: PropTypes.bool.isRequired, 77 | onFormSubmit: PropTypes.func.isRequired, 78 | content: PropTypes.element.isRequired, 79 | actions: PropTypes.element.isRequired, 80 | hideBackdrop: PropTypes.bool.isRequired 81 | }; 82 | 83 | export default withStyles(styles, { withTheme: true })(FormDialog); 84 | -------------------------------------------------------------------------------- /src/shared/components/HelpIcon.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Tooltip, Typography, withStyles } from "@material-ui/core"; 4 | import HelpIconOutline from "@material-ui/icons/HelpOutline"; 5 | 6 | const styles = theme => ({ 7 | tooltipTypo: { 8 | whiteSpace: "pre-line !important", 9 | ...theme.typography.caption, 10 | color: theme.palette.common.white 11 | }, 12 | tooltip: { 13 | verticalAlign: "middle", 14 | fontSize: "1.25rem" 15 | }, 16 | helpIcon: { 17 | marginLeft: theme.spacing(1), 18 | "@media (max-width: 350px)": { 19 | marginLeft: theme.spacing(0.5) 20 | }, 21 | transition: theme.transitions.create(["color"], { 22 | duration: theme.transitions.duration.short, 23 | easing: theme.transitions.easing.easeInOut 24 | }) 25 | } 26 | }); 27 | 28 | function HelpIcon(props) { 29 | const { classes, title } = props; 30 | const [isHovered, setIsHovered] = useState(false); 31 | 32 | const onMouseOver = useCallback(() => { 33 | setIsHovered(true); 34 | }, []); 35 | 36 | const onMouseLeave = useCallback(() => { 37 | setIsHovered(false); 38 | }, []); 39 | 40 | return ( 41 | 44 | {title} 45 | 46 | } 47 | className={classes.tooltip} 48 | enterTouchDelay={300} 49 | > 50 | 63 | 64 | ); 65 | } 66 | 67 | HelpIcon.propTypes = { 68 | classes: PropTypes.object, 69 | title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) 70 | }; 71 | 72 | export default withStyles(styles, { withTheme: true })(HelpIcon); 73 | -------------------------------------------------------------------------------- /src/shared/components/HighlightedInformation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { Typography, withStyles } from "@material-ui/core"; 5 | 6 | const styles = theme => ({ 7 | main: { 8 | backgroundColor: theme.palette.warning.light, 9 | border: `${theme.border.borderWidth}px solid ${theme.palette.warning.main}`, 10 | padding: theme.spacing(2), 11 | borderRadius: theme.shape.borderRadius 12 | } 13 | }); 14 | 15 | function HighlighedInformation(props) { 16 | const { className, children, classes } = props; 17 | return ( 18 |
19 | {children} 20 |
21 | ); 22 | } 23 | 24 | HighlighedInformation.propTypes = { 25 | classes: PropTypes.object.isRequired, 26 | children: PropTypes.oneOfType([ 27 | PropTypes.string, 28 | PropTypes.element, 29 | PropTypes.array 30 | ]).isRequired, 31 | className: PropTypes.string 32 | }; 33 | 34 | export default withStyles(styles, { withTheme: true })(HighlighedInformation); 35 | -------------------------------------------------------------------------------- /src/shared/components/ImageCropperDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Box, 9 | withStyles, 10 | } from "@material-ui/core"; 11 | 12 | const styles = (theme) => ({ 13 | dialogPaper: { maxWidth: `${theme.breakpoints.values.md}px !important` }, 14 | dialogContent: { 15 | paddingTop: theme.spacing(2), 16 | paddingRight: theme.spacing(2), 17 | paddingLeft: theme.spacing(2), 18 | }, 19 | }); 20 | 21 | function ImageCropperDialog(props) { 22 | const { 23 | ImageCropper, 24 | classes, 25 | onClose, 26 | open, 27 | src, 28 | onCrop, 29 | aspectRatio, 30 | theme, 31 | } = props; 32 | const [crop, setCrop] = useState(null); 33 | 34 | const getCropFunctionFromChild = useCallback( 35 | (cropFunction) => { 36 | setCrop(() => cropFunction); 37 | }, 38 | [setCrop] 39 | ); 40 | 41 | return ( 42 | 48 | 49 | 56 | 57 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | ); 67 | } 68 | 69 | ImageCropperDialog.propTypes = { 70 | ImageCropper: PropTypes.elementType, 71 | classes: PropTypes.object.isRequired, 72 | onClose: PropTypes.func.isRequired, 73 | open: PropTypes.bool.isRequired, 74 | onCrop: PropTypes.func.isRequired, 75 | src: PropTypes.string, 76 | aspectRatio: PropTypes.number, 77 | }; 78 | 79 | export default withStyles(styles, { withTheme: true })(ImageCropperDialog); 80 | -------------------------------------------------------------------------------- /src/shared/components/ModalBackdrop.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Backdrop, withStyles } from "@material-ui/core"; 4 | 5 | const styles = { 6 | backdrop: { 7 | top: 0, 8 | left: 0, 9 | right: 0, 10 | bottom: 0, 11 | zIndex: 1200, 12 | position: "fixed", 13 | touchAction: "none", 14 | backgroundColor: "rgba(0, 0, 0, 0.5)" 15 | } 16 | }; 17 | 18 | function ModalBackdrop(props) { 19 | const { classes, open } = props; 20 | return ; 21 | } 22 | 23 | ModalBackdrop.propTypes = { 24 | classes: PropTypes.object.isRequired, 25 | open: PropTypes.bool.isRequired 26 | }; 27 | 28 | export default withStyles(styles)(ModalBackdrop); 29 | -------------------------------------------------------------------------------- /src/shared/components/NavigationDrawer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } 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 | withStyles, 11 | IconButton, 12 | Typography, 13 | withWidth, 14 | isWidthUp, 15 | Toolbar 16 | } from "@material-ui/core"; 17 | import CloseIcon from "@material-ui/icons/Close"; 18 | 19 | const styles = theme => ({ 20 | closeIcon: { 21 | marginRight: theme.spacing(0.5) 22 | }, 23 | headSection: { 24 | width: 200 25 | }, 26 | blackList: { 27 | backgroundColor: theme.palette.common.black, 28 | height: "100%" 29 | }, 30 | noDecoration: { 31 | textDecoration: "none !important" 32 | } 33 | }); 34 | 35 | function NavigationDrawer(props) { 36 | const { 37 | width, 38 | open, 39 | onClose, 40 | anchor, 41 | classes, 42 | menuItems, 43 | selectedItem, 44 | theme 45 | } = props; 46 | 47 | useEffect(() => { 48 | window.onresize = () => { 49 | if (isWidthUp("sm", width) && open) { 50 | onClose(); 51 | } 52 | }; 53 | }, [width, open, onClose]); 54 | 55 | return ( 56 | 57 | 58 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {menuItems.map(element => { 76 | if (element.link) { 77 | return ( 78 | 84 | 94 | {element.icon} 95 | 98 | {element.name} 99 | 100 | } 101 | /> 102 | 103 | 104 | ); 105 | } 106 | return ( 107 | 108 | {element.icon} 109 | 112 | {element.name} 113 | 114 | } 115 | /> 116 | 117 | ); 118 | })} 119 | 120 | 121 | ); 122 | } 123 | 124 | NavigationDrawer.propTypes = { 125 | anchor: PropTypes.string.isRequired, 126 | theme: PropTypes.object.isRequired, 127 | open: PropTypes.bool.isRequired, 128 | onClose: PropTypes.func.isRequired, 129 | menuItems: PropTypes.arrayOf(PropTypes.object).isRequired, 130 | classes: PropTypes.object.isRequired, 131 | width: PropTypes.string.isRequired, 132 | selectedItem: PropTypes.string 133 | }; 134 | 135 | export default withWidth()( 136 | withStyles(styles, { withTheme: true })(NavigationDrawer) 137 | ); 138 | -------------------------------------------------------------------------------- /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 { GridListTileBar, withStyles } from "@material-ui/core"; 5 | import VertOptions from "./VertOptions"; 6 | 7 | const styles = { 8 | imageContainer: { 9 | width: "100%", 10 | paddingTop: "100%", 11 | overflow: "hidden", 12 | position: "relative", 13 | }, 14 | image: { 15 | position: "absolute", 16 | top: 0, 17 | bottom: 0, 18 | left: 0, 19 | right: 0, 20 | margin: "auto", 21 | }, 22 | }; 23 | 24 | function SelfAligningImage(props) { 25 | const { 26 | classes, 27 | src, 28 | title, 29 | timeStamp, 30 | options, 31 | roundedBorder, 32 | theme, 33 | } = props; 34 | const img = useRef(); 35 | const [hasMoreWidthThanHeight, setHasMoreWidthThanHeight] = useState(null); 36 | const [hasLoaded, setHasLoaded] = useState(false); 37 | 38 | const onLoad = useCallback(() => { 39 | if (img.current.naturalHeight < img.current.naturalWidth) { 40 | setHasMoreWidthThanHeight(true); 41 | } else { 42 | setHasMoreWidthThanHeight(false); 43 | } 44 | setHasLoaded(true); 45 | }, [img, setHasLoaded, setHasMoreWidthThanHeight]); 46 | 47 | return ( 48 |
49 | 62 | {title && ( 63 | 0 && ( 70 | 71 | ) 72 | } 73 | /> 74 | )} 75 |
76 | ); 77 | } 78 | 79 | SelfAligningImage.propTypes = { 80 | classes: PropTypes.object.isRequired, 81 | src: PropTypes.string.isRequired, 82 | theme: PropTypes.object.isRequired, 83 | title: PropTypes.string, 84 | timeStamp: PropTypes.number, 85 | roundedBorder: PropTypes.bool, 86 | options: PropTypes.arrayOf(PropTypes.object), 87 | }; 88 | 89 | export default withStyles(styles, { withTheme: true })(SelfAligningImage); 90 | -------------------------------------------------------------------------------- /src/shared/components/VertOptions.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Popover, 5 | IconButton, 6 | MenuList, 7 | ListItemText, 8 | ListItemIcon, 9 | MenuItem, 10 | withStyles, 11 | } from "@material-ui/core"; 12 | import MoreVertIcon from "@material-ui/icons/MoreVert"; 13 | 14 | const styles = { 15 | listItemtext: { 16 | paddingLeft: "0 !important", 17 | }, 18 | }; 19 | 20 | function VertOptions(props) { 21 | const { items, classes, color } = props; 22 | const anchorEl = useRef(); 23 | const [isOpen, setIsOpen] = useState(false); 24 | 25 | const handleClose = useCallback(() => { 26 | setIsOpen(false); 27 | }, [setIsOpen]); 28 | 29 | const handleOpen = useCallback(() => { 30 | setIsOpen(true); 31 | }, [setIsOpen]); 32 | 33 | const id = isOpen ? "scroll-playground" : null; 34 | return ( 35 | 36 | 43 | 44 | 45 | 60 | 61 | {items.map((item) => ( 62 | { 65 | handleClose(); 66 | item.onClick(); 67 | }} 68 | > 69 | {item.icon} 70 | 71 | {item.name} 72 | 73 | 74 | ))} 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | VertOptions.propTypes = { 82 | items: PropTypes.arrayOf(PropTypes.object).isRequired, 83 | classes: PropTypes.object.isRequired, 84 | color: PropTypes.string, 85 | }; 86 | 87 | export default withStyles(styles)(VertOptions); 88 | -------------------------------------------------------------------------------- /src/shared/components/VisibilityPasswordTextField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField, InputAdornment, IconButton } from "@material-ui/core"; 3 | import VisibilityIcon from "@material-ui/icons/Visibility"; 4 | import VisibilityOffIcon from "@material-ui/icons/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 | > 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 "@material-ui/core"; 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, withStyles } from "@material-ui/core"; 4 | import ScrollbarSize from "@material-ui/core/Tabs/ScrollbarSize"; 5 | import classNames from "classnames"; 6 | 7 | const styles = (theme) => ({ 8 | backdrop: { 9 | zIndex: theme.zIndex.modal, 10 | backgroundColor: "rgba(0, 0, 0, 0.8)", 11 | }, 12 | portalImgWrapper: { 13 | position: "fixed", 14 | top: "0", 15 | left: "0", 16 | width: "100%", 17 | height: "100%", 18 | zIndex: theme.zIndex.modal, 19 | cursor: "pointer", 20 | }, 21 | portalImgInnerWrapper: { 22 | display: "flex", 23 | justifyContent: "center", 24 | alignItems: "center", 25 | width: "100%", 26 | height: "100%", 27 | paddingLeft: theme.spacing(1), 28 | paddingRight: theme.spacing(1), 29 | paddingTop: theme.spacing(1), 30 | paddingBottom: theme.spacing(1), 31 | }, 32 | portalImg: { 33 | objectFit: "contain", 34 | maxWidth: "100%", 35 | maxHeight: "100%", 36 | }, 37 | zoomedOutImage: { 38 | cursor: "pointer", 39 | }, 40 | }); 41 | 42 | function ZoomImage(props) { 43 | const { alt, src, zoomedImgProps, classes, className, ...rest } = props; 44 | const [zoomedIn, setZoomedIn] = useState(false); 45 | const [scrollbarSize, setScrollbarSize] = useState(null); 46 | 47 | const zoomIn = useCallback(() => { 48 | setZoomedIn(true); 49 | }, [setZoomedIn]); 50 | 51 | const zoomOut = useCallback(() => { 52 | setZoomedIn(false); 53 | }, [setZoomedIn]); 54 | 55 | useEffect(() => { 56 | if (zoomedIn) { 57 | document.body.style.overflow = "hidden"; 58 | document.body.style.paddingRight = `${scrollbarSize}px`; 59 | document.querySelector( 60 | "header" 61 | ).style.paddingRight = `${scrollbarSize}px`; 62 | } else { 63 | document.body.style.overflow = "auto"; 64 | document.body.style.paddingRight = "0px"; 65 | document.querySelector("header").style.paddingRight = "0px"; 66 | } 67 | }, [zoomedIn, scrollbarSize]); 68 | 69 | return ( 70 | 71 | 72 | {zoomedIn && ( 73 | 74 | 79 |
80 |
81 | {alt} 87 |
88 |
89 |
90 | )} 91 | {alt} 98 |
99 | ); 100 | } 101 | 102 | ZoomImage.propTypes = { 103 | classes: PropTypes.object.isRequired, 104 | alt: PropTypes.string.isRequired, 105 | src: PropTypes.string.isRequired, 106 | theme: PropTypes.object.isRequired, 107 | zoomedImgProps: PropTypes.object, 108 | className: PropTypes.string, 109 | }; 110 | 111 | export default withStyles(styles, { withTheme: true })(ZoomImage); 112 | -------------------------------------------------------------------------------- /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/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, responsiveFontSizes } from "@material-ui/core"; 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 = createMuiTheme({ 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 | --------------------------------------------------------------------------------