├── .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 | 
7 | [](https://dependabot.com)
8 | [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |

87 |
88 |
89 |
90 | )}
91 |
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 |
--------------------------------------------------------------------------------