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

63 | {title && (
64 |
0 && (
71 |
72 | )
73 | }
74 | />
75 | )}
76 |
77 | );
78 | }
79 |
80 | SelfAligningImage.propTypes = {
81 | classes: PropTypes.object.isRequired,
82 | src: PropTypes.string.isRequired,
83 | theme: PropTypes.object.isRequired,
84 | title: PropTypes.string,
85 | timeStamp: PropTypes.number,
86 | roundedBorder: PropTypes.bool,
87 | options: PropTypes.arrayOf(PropTypes.object),
88 | };
89 |
90 | export default withStyles(styles, { withTheme: true })(SelfAligningImage);
91 |
--------------------------------------------------------------------------------
/src/shared/components/VertOptions.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState, useCallback, useRef } from "react";
2 | import PropTypes from "prop-types";
3 | import { Popover, IconButton, MenuList, ListItemText, ListItemIcon, MenuItem } from "@mui/material";
4 | import withStyles from '@mui/styles/withStyles';
5 | import MoreVertIcon from "@mui/icons-material/MoreVert";
6 |
7 | const styles = {
8 | listItemtext: {
9 | paddingLeft: "0 !important",
10 | },
11 | };
12 |
13 | function VertOptions(props) {
14 | const { items, classes, color } = props;
15 | const anchorEl = useRef();
16 | const [isOpen, setIsOpen] = useState(false);
17 |
18 | const handleClose = useCallback(() => {
19 | setIsOpen(false);
20 | }, [setIsOpen]);
21 |
22 | const handleOpen = useCallback(() => {
23 | setIsOpen(true);
24 | }, [setIsOpen]);
25 |
26 | const id = isOpen ? "scroll-playground" : null;
27 | return (
28 |
29 |
36 |
37 |
38 |
53 |
54 | {items.map((item) => (
55 |
67 | ))}
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | VertOptions.propTypes = {
75 | items: PropTypes.arrayOf(PropTypes.object).isRequired,
76 | classes: PropTypes.object.isRequired,
77 | color: PropTypes.string,
78 | };
79 |
80 | export default withStyles(styles)(VertOptions);
81 |
--------------------------------------------------------------------------------
/src/shared/components/VisibilityPasswordTextField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TextField, InputAdornment, IconButton } from "@mui/material";
3 | import VisibilityIcon from "@mui/icons-material/Visibility";
4 | import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
5 |
6 | function VisibilityPasswordTextField(props) {
7 | const { isVisible, onVisibilityChange, ...rest } = props;
8 | return (
9 |
15 | {
18 | onVisibilityChange(!isVisible);
19 | }}
20 | onMouseDown={(event) => {
21 | event.preventDefault();
22 | }}
23 | size="large">
24 | {isVisible ? : }
25 |
26 |
27 | ),
28 | }}
29 | >
30 | );
31 | }
32 |
33 | export default VisibilityPasswordTextField;
34 |
--------------------------------------------------------------------------------
/src/shared/components/WaveBorder.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import withStyles from '@mui/styles/withStyles';
4 |
5 | const styles = {
6 | waves: {
7 | position: "relative",
8 | width: "100%",
9 | marginBottom: -7,
10 | height: "7vw",
11 | minHeight: "7vw"
12 | },
13 | "@keyframes moveForever": {
14 | from: { transform: "translate3d(-90px, 0, 0)" },
15 | to: { transform: "translate3d(85px, 0, 0)" }
16 | },
17 | parallax: {
18 | "& > use": {
19 | animation: "$moveForever 4s cubic-bezier(0.62, 0.5, 0.38, 0.5) infinite",
20 | animationDelay: props => `-${props.animationNegativeDelay}s`
21 | }
22 | }
23 | };
24 |
25 | /**
26 | * https://codepen.io/csspoints/pen/WNeOEqd
27 | */
28 | function WaveBorder(props) {
29 | const id = String(Math.random());
30 | const {
31 | className,
32 | lowerColor,
33 | upperColor,
34 | classes,
35 | animationNegativeDelay,
36 | ...rest
37 | } = props;
38 | return (
39 |
40 |
58 |
59 | );
60 | }
61 |
62 | WaveBorder.propTypes = {
63 | lowerColor: PropTypes.string.isRequired,
64 | upperColor: PropTypes.string.isRequired,
65 | classes: PropTypes.object.isRequired,
66 | animationNegativeDelay: PropTypes.number.isRequired
67 | };
68 |
69 | export default withStyles(styles)(WaveBorder);
70 |
--------------------------------------------------------------------------------
/src/shared/components/ZoomImage.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState, useCallback, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { Portal, Backdrop } from "@mui/material";
4 | import withStyles from "@mui/styles/withStyles";
5 | import ScrollbarSize from "@mui/material/Tabs/ScrollbarSize";
6 | import classNames from "classnames";
7 |
8 | const styles = (theme) => ({
9 | backdrop: {
10 | zIndex: theme.zIndex.modal,
11 | backgroundColor: "rgba(0, 0, 0, 0.8)",
12 | },
13 | portalImgWrapper: {
14 | position: "fixed",
15 | top: "0",
16 | left: "0",
17 | width: "100%",
18 | height: "100%",
19 | zIndex: theme.zIndex.modal,
20 | cursor: "pointer",
21 | },
22 | portalImgInnerWrapper: {
23 | display: "flex",
24 | justifyContent: "center",
25 | alignItems: "center",
26 | width: "100%",
27 | height: "100%",
28 | paddingLeft: theme.spacing(1),
29 | paddingRight: theme.spacing(1),
30 | paddingTop: theme.spacing(1),
31 | paddingBottom: theme.spacing(1),
32 | },
33 | portalImg: {
34 | objectFit: "contain",
35 | maxWidth: "100%",
36 | maxHeight: "100%",
37 | },
38 | zoomedOutImage: {
39 | cursor: "pointer",
40 | },
41 | });
42 |
43 | function ZoomImage(props) {
44 | const { alt, src, zoomedImgProps, classes, className, ...rest } = props;
45 | const [zoomedIn, setZoomedIn] = useState(false);
46 | const [scrollbarSize, setScrollbarSize] = useState(null);
47 |
48 | const zoomIn = useCallback(() => {
49 | setZoomedIn(true);
50 | }, [setZoomedIn]);
51 |
52 | const zoomOut = useCallback(() => {
53 | setZoomedIn(false);
54 | }, [setZoomedIn]);
55 |
56 | useEffect(() => {
57 | if (zoomedIn) {
58 | document.body.style.overflow = "hidden";
59 | document.body.style.paddingRight = `${scrollbarSize}px`;
60 | document.querySelector(
61 | "header"
62 | ).style.paddingRight = `${scrollbarSize}px`;
63 | } else {
64 | document.body.style.overflow = "auto";
65 | document.body.style.paddingRight = "0px";
66 | document.querySelector("header").style.paddingRight = "0px";
67 | }
68 | }, [zoomedIn, scrollbarSize]);
69 |
70 | return (
71 |
72 |
73 | {zoomedIn && (
74 |
75 |
80 |
81 |
82 |

88 |
89 |
90 |
91 | )}
92 |
99 |
100 | );
101 | }
102 |
103 | ZoomImage.propTypes = {
104 | classes: PropTypes.object.isRequired,
105 | alt: PropTypes.string.isRequired,
106 | src: PropTypes.string.isRequired,
107 | theme: PropTypes.object.isRequired,
108 | zoomedImgProps: PropTypes.object,
109 | className: PropTypes.string,
110 | };
111 |
112 | export default withStyles(styles, { withTheme: true })(ZoomImage);
113 |
--------------------------------------------------------------------------------
/src/shared/functions/countWithEmojis.js:
--------------------------------------------------------------------------------
1 | import toArray from "./toArray";
2 |
3 | /**
4 | * Counts the characters in a string and counts emojis correctly.
5 | *
6 | * @param {string} str The string to count characters from.
7 | * @return {number} The number of characters in the string.
8 | */
9 | function countWithEmojis(str) {
10 | return toArray(str).length;
11 | }
12 |
13 | export default countWithEmojis;
14 |
--------------------------------------------------------------------------------
/src/shared/functions/currencyPrettyPrint.js:
--------------------------------------------------------------------------------
1 | function currencyPrettyPrint(cents) {
2 | const dollars = cents / 100;
3 | return dollars.toLocaleString("en-US", {
4 | style: "currency",
5 | currency: "USD"
6 | });
7 | }
8 |
9 | export default currencyPrettyPrint;
10 |
--------------------------------------------------------------------------------
/src/shared/functions/getSorting.js:
--------------------------------------------------------------------------------
1 | function desc(a, b, orderBy) {
2 | if (b[orderBy] < a[orderBy]) {
3 | return -1;
4 | }
5 | if (b[orderBy] > a[orderBy]) {
6 | return 1;
7 | }
8 | return 0;
9 | }
10 |
11 | function getSorting(order, orderBy) {
12 | return order === "desc"
13 | ? (a, b) => desc(a, b, orderBy)
14 | : (a, b) => -desc(a, b, orderBy);
15 | }
16 | export default getSorting;
17 |
--------------------------------------------------------------------------------
/src/shared/functions/shadeColor.js:
--------------------------------------------------------------------------------
1 | function shadeColor(color, percent) {
2 | const f = parseInt(color.slice(1), 16);
3 | const t = percent < 0 ? 0 : 255;
4 | const p = percent < 0 ? percent * -1 : percent;
5 | const R = f >> 16;
6 | const G = (f >> 8) & 0x00ff;
7 | const B = f & 0x0000ff;
8 | return `#${(
9 | 0x1000000 +
10 | (Math.round((t - R) * p) + R) * 0x10000 +
11 | (Math.round((t - G) * p) + G) * 0x100 +
12 | (Math.round((t - B) * p) + B)
13 | )
14 | .toString(16)
15 | .slice(1)}`;
16 | }
17 |
18 | export default shadeColor;
19 |
--------------------------------------------------------------------------------
/src/shared/functions/smoothScrollTop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * When called scrolls smooth to the top of the page.
3 | * globLastC prevents shaky animations when scrolling to
4 | * bottom while topscrolling.
5 | */
6 | let globLastC = Infinity;
7 |
8 | function smoothScrollTopRec() {
9 | const c = document.documentElement.scrollTop || document.body.scrollTop;
10 | if (c > 0 && globLastC > c) {
11 | globLastC = c;
12 | window.requestAnimationFrame(smoothScrollTopRec);
13 | window.scrollTo(0, c - c / 8);
14 | } else {
15 | globLastC = Infinity;
16 | }
17 | }
18 |
19 | function smoothScrollTop() {
20 | /**
21 | * Normally this gets called from componentDidMount()
22 | * Not waiting a tiny fraction of time can lead
23 | * to shaky behaviour
24 | */
25 | setTimeout(() => {
26 | smoothScrollTopRec();
27 | }, 10);
28 | }
29 |
30 | export default smoothScrollTop;
31 |
--------------------------------------------------------------------------------
/src/shared/functions/stableSort.js:
--------------------------------------------------------------------------------
1 | function stableSort(array, cmp) {
2 | const stabilizedThis = array.map((el, index) => [el, index]);
3 | stabilizedThis.sort((a, b) => {
4 | const order = cmp(a[0], b[0]);
5 | if (order !== 0) return order;
6 | return a[1] - b[1];
7 | });
8 | return stabilizedThis.map(el => el[0]);
9 | }
10 | export default stableSort;
11 |
--------------------------------------------------------------------------------
/src/shared/functions/unixToDateString.js:
--------------------------------------------------------------------------------
1 | function unixToDateString(unix) {
2 | const date = new Date(unix * 1000);
3 | return (
4 | `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`
5 | );
6 | }
7 |
8 | export default unixToDateString;
9 |
--------------------------------------------------------------------------------
/src/shared/functions/useLocationBlocker.js:
--------------------------------------------------------------------------------
1 | import { useHistory } from "react-router-dom";
2 | import { useEffect } from "react";
3 |
4 |
5 | const useLocationBlocker = () => {
6 | /**
7 | * Prevents react-router from pushing the same
8 | * page to the history twice which leads to
9 | * multiple clicks on the back icon of the browser
10 | * being necessary to go back into the history.
11 | */
12 | const history = useHistory();
13 | useEffect(
14 | () =>
15 | history.block(
16 | (location, action) =>
17 | action !== "PUSH" ||
18 | getLocationId(location) !== getLocationId(history.location)
19 | ),
20 | [] // eslint-disable-line react-hooks/exhaustive-deps
21 | );
22 | }
23 |
24 | const getLocationId = ({ pathname, search, hash }) => {
25 | return pathname + (search ? "?" + search : "") + (hash ? "#" + hash : "");
26 | }
27 |
28 | export default useLocationBlocker;
--------------------------------------------------------------------------------
/src/shared/functions/useWidth.js:
--------------------------------------------------------------------------------
1 | import { useTheme } from "@mui/material/styles";
2 | import useMediaQuery from "@mui/material/useMediaQuery";
3 |
4 | /**
5 | * Be careful using this hook. It only works because the number of
6 | * breakpoints in theme is static. It will break once you change the number of
7 | * breakpoints. See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
8 | */
9 | function useWidth() {
10 | const theme = useTheme();
11 | const keys = [...theme.breakpoints.keys].reverse();
12 | return (
13 | keys.reduce((output, key) => {
14 | // eslint-disable-next-line react-hooks/rules-of-hooks
15 | const matches = useMediaQuery(theme.breakpoints.up(key));
16 | return !output && matches ? key : output;
17 | }, null) || "xs"
18 | );
19 | }
20 |
21 | export default useWidth;
22 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import { createTheme, responsiveFontSizes, adaptV4Theme } from "@mui/material";
2 |
3 | // colors
4 | const primary = "#b3294e";
5 | const secondary = "#4829B2";
6 | const black = "#343a40";
7 | const darkBlack = "rgb(36, 40, 44)";
8 | const background = "#f5f5f5";
9 | const warningLight = "rgba(253, 200, 69, .3)";
10 | const warningMain = "rgba(253, 200, 69, .5)";
11 | const warningDark = "rgba(253, 200, 69, .7)";
12 |
13 | // border
14 | const borderWidth = 2;
15 | const borderColor = "rgba(0, 0, 0, 0.13)";
16 |
17 | // breakpoints
18 | const xl = 1920;
19 | const lg = 1280;
20 | const md = 960;
21 | const sm = 600;
22 | const xs = 0;
23 |
24 | // spacing
25 | const spacing = 8;
26 |
27 | const theme = createTheme(adaptV4Theme({
28 | palette: {
29 | primary: { main: primary },
30 | secondary: { main: secondary },
31 | common: {
32 | black,
33 | darkBlack
34 | },
35 | warning: {
36 | light: warningLight,
37 | main: warningMain,
38 | dark: warningDark
39 | },
40 | // Used to shift a color's luminance by approximately
41 | // two indexes within its tonal palette.
42 | // E.g., shift from Red 500 to Red 300 or Red 700.
43 | tonalOffset: 0.2,
44 | background: {
45 | default: background
46 | },
47 | spacing
48 | },
49 | breakpoints: {
50 | // Define custom breakpoint values.
51 | // These will apply to Material-UI components that use responsive
52 | // breakpoints, such as `Grid` and `Hidden`. You can also use the
53 | // theme breakpoint functions `up`, `down`, and `between` to create
54 | // media queries for these breakpoints
55 | values: {
56 | xl,
57 | lg,
58 | md,
59 | sm,
60 | xs
61 | }
62 | },
63 | border: {
64 | borderColor: borderColor,
65 | borderWidth: borderWidth
66 | },
67 | overrides: {
68 | MuiExpansionPanel: {
69 | root: {
70 | position: "static"
71 | }
72 | },
73 | MuiTableCell: {
74 | root: {
75 | paddingLeft: spacing * 2,
76 | paddingRight: spacing * 2,
77 | borderBottom: `${borderWidth}px solid ${borderColor}`,
78 | [`@media (max-width: ${sm}px)`]: {
79 | paddingLeft: spacing,
80 | paddingRight: spacing
81 | }
82 | }
83 | },
84 | MuiDivider: {
85 | root: {
86 | backgroundColor: borderColor,
87 | height: borderWidth
88 | }
89 | },
90 | MuiPrivateNotchedOutline: {
91 | root: {
92 | borderWidth: borderWidth
93 | }
94 | },
95 | MuiListItem: {
96 | divider: {
97 | borderBottom: `${borderWidth}px solid ${borderColor}`
98 | }
99 | },
100 | MuiDialog: {
101 | paper: {
102 | width: "100%",
103 | maxWidth: 430,
104 | marginLeft: spacing,
105 | marginRight: spacing
106 | }
107 | },
108 | MuiTooltip: {
109 | tooltip: {
110 | backgroundColor: darkBlack
111 | }
112 | },
113 | MuiExpansionPanelDetails: {
114 | root: {
115 | [`@media (max-width: ${sm}px)`]: {
116 | paddingLeft: spacing,
117 | paddingRight: spacing
118 | }
119 | }
120 | }
121 | },
122 | typography: {
123 | useNextVariants: true
124 | }
125 | }));
126 |
127 | export default responsiveFontSizes(theme);
128 |
--------------------------------------------------------------------------------