├── .gitignore ├── README.md ├── md ├── access.png ├── css.png ├── example1.png ├── html.png ├── lighthouse.png ├── stage1.png ├── stage2.png ├── stage3.png ├── test1.png └── test2.png ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json ├── mstile-150x150.png ├── robots.txt └── safari-pinned-tab.svg └── src ├── App.js ├── App.test.js ├── assets ├── css │ └── normalize.css ├── imgs │ └── green-tick.svg └── scss │ ├── _base.scss │ ├── _buttons.scss │ ├── _mixin.scss │ ├── _progress-bar.scss │ ├── _variables.scss │ └── main.scss ├── components ├── form-completed │ ├── index.js │ └── styles.scss ├── form-privacy │ ├── index.js │ └── styles.scss └── form-signup │ ├── index.js │ └── styles.scss ├── index.js ├── service-worker.js ├── serviceWorkerRegistration.js ├── setupTests.js ├── store ├── index.js └── rootSlice.js └── views └── signup ├── index.js └── styles.scss /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux Multi-Step Signup Form 2 | __[Live Link](https://unruffled-mcnulty-71b799.netlify.app)__ 3 |
4 | 5 | Created with React, Redux Toolkit, and SASS. Plus React Lazy Load for Component loading. 6 |

7 | 8 | ## Questions 9 | ### How you would change the configuration of a certain page? 10 | The components allow very easy control via props to change page titles, submit button text, success message and even enable dynamic back buttons too. 11 |
12 | 13 | ### How you would add new pages? 14 | The app uses a 'views' approach, so new components can easily be added to the signup view page by linking to new components from the components directory. Then also adding them to the progress component in the signup views page too. 15 |
16 | 17 | ### How you would implement going back a page? 18 | The components feature props to enable/disable a dynamic 'Back' button as outlined in the prop documention below. 19 |

20 | 21 | 22 | ## Features 23 | - Multi-Step Signup Form 24 | - Form Progression Path 25 | - Modular/Scalable App 26 | - Form Validation 27 | - Custom fav icon 28 | - Lazy Loading for image and components 29 | - React Testing Library pass 30 | - PWA testing pass 31 | - Lighthouse testing pass 32 | - HTML testing pass 33 | - CSS testing pass 34 | - Accessibility testing pass 35 |
36 | 37 | ## Run 38 | ````cmd 39 | npm install 40 | npm start 41 | ```` 42 | ![Example1](./md/example1.png) 43 | 44 | 45 | ## Components 46 | 47 | ### Form User Signup Component 48 | Component for Signup Page 49 | ![Component for Signup Page](./md/stage1.png) 50 | | Prop Name | Description | Example | Type | 51 | | ------------- |:-------------:| -----:| -----:| 52 | | pageTitle | form page stage title | {'User Form:'} | `string` | 53 | | submitButtonText | submit next button display text | {'Next'} | `string` | 54 | | previousButton | shows / hides Back button | {false} | `boolean` | 55 | 56 |
57 | 58 | ### Form User Privacy Component 59 | Component for Privacy Page 60 | ![Component for Privacy Page](./md/stage2.png) 61 | | Prop Name | Description | Example | Type | 62 | | ------------- |:-------------:| -----:| -----:| 63 | | pageTitle | form page stage title | {'Privacy Form:'} | `string` | 64 | | submitButtonText | submit next button display text | {'Next'} | `string` | 65 | | previousButton | shows / hides Back button | {true} | `boolean` | 66 | 67 |
68 | 69 | ### Form User Completion Component 70 | Component for Completion Page 71 | ![Component for Completion Page](./md/stage3.png) 72 | | Prop Name | Description | Example | Type | 73 | | ------------- |:-------------:| -----:| -----:| 74 | | pageTitle | form page stage title | {'Success!'} | `string` | 75 | | successMessage | Success message to display | {'Thanks for your submission'} | `string` | 76 | 77 |
78 | 79 | 80 | ## Testing 81 | __React Testing Library__ 82 |

83 | run `npm test` to perform testing 84 |
85 | Basic test to check page h1 title loads with test id. 86 |
87 | ![React Testing Library 1](./md/test1.png) 88 | ![React Testing Library 2](./md/test2.png) 89 | 90 | 91 | ## Other Testing 92 | 93 | __Google Lighthouse__ 94 |
95 | ![Google Lighthouse](./md/lighthouse.png) 96 | 97 | __[Accessiblity Testing Link](https://wave.webaim.org/report#/https://unruffled-mcnulty-71b799.netlify.app/)__ 98 | ![Google Lighthouse](./md/access.png) 99 | 100 | __[CSS Testing Link](https://jigsaw.w3.org/css-validator/validator?profile=css3&warning=0&uri=https://unruffled-mcnulty-71b799.netlify.app/)__ 101 | ![Google Lighthouse](./md/css.png) 102 | 103 | __[HTML Testing Link](https://validator.w3.org/nu/?doc=https://unruffled-mcnulty-71b799.netlify.app/)__ 104 | ![Google Lighthouse](./md/html.png) 105 | -------------------------------------------------------------------------------- /md/access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/access.png -------------------------------------------------------------------------------- /md/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/css.png -------------------------------------------------------------------------------- /md/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/example1.png -------------------------------------------------------------------------------- /md/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/html.png -------------------------------------------------------------------------------- /md/lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/lighthouse.png -------------------------------------------------------------------------------- /md/stage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/stage1.png -------------------------------------------------------------------------------- /md/stage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/stage2.png -------------------------------------------------------------------------------- /md/stage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/stage3.png -------------------------------------------------------------------------------- /md/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/test1.png -------------------------------------------------------------------------------- /md/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/md/test2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trayio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.6.1", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-lazyload": "^3.2.0", 13 | "react-redux": "^7.2.5", 14 | "react-scripts": "4.0.3", 15 | "web-vitals": "^0.2.4", 16 | "workbox-background-sync": "^5.1.4", 17 | "workbox-broadcast-update": "^5.1.4", 18 | "workbox-cacheable-response": "^5.1.4", 19 | "workbox-core": "^5.1.4", 20 | "workbox-expiration": "^5.1.4", 21 | "workbox-google-analytics": "^5.1.4", 22 | "workbox-navigation-preload": "^5.1.4", 23 | "workbox-precaching": "^5.1.4", 24 | "workbox-range-requests": "^5.1.4", 25 | "workbox-routing": "^5.1.4", 26 | "workbox-strategies": "^5.1.4", 27 | "workbox-streams": "^5.1.4" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "sass": "^1.42.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Redux Multi-Step Signup Form 8 | 12 | 16 | 21 | 27 | 33 | 38 | 39 | 48 | 49 | 50 |
51 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Redux Signup Form", 3 | "name": "Redux Multi-Step Signup Form", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbhachu/redux-multi-step-form/3dbdb070164d3e445c0528973afa3f5b09282376/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import Signup from './views/signup'; // load view 2 | 3 | function App() { 4 | 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | import { Provider } from "react-redux"; 4 | import { store } from './store' 5 | 6 | test('check page h1 title loads', async () => { 7 | render( 8 | 9 | 10 | ); 11 | const element = screen.getByTestId('Signup-Title') 12 | expect(element).toBeInTheDocument(); 13 | }); -------------------------------------------------------------------------------- /src/assets/css/normalize.css: -------------------------------------------------------------------------------- 1 | /* normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | /** 6 | * 1. Add border box sizing in all browsers (opinionated). 7 | */ 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; /* 1 */ 12 | } 13 | /** 14 | * 1. Correct the line height in all browsers. 15 | * 2. Prevent adjustments of font size after orientation changes in iOS. 16 | */ 17 | html { 18 | line-height: 1.15; /* 1 */ 19 | -webkit-text-size-adjust: 100%; /* 2 */ 20 | } 21 | /* Sections 22 | ========================================================================== */ 23 | /** 24 | * Remove the margin in all browsers. 25 | * Correct the font size and margin on `h1` elements within `section` and 26 | * `article` contexts in Chrome, Firefox, and Safari. 27 | */ 28 | body, h1, h2, h3, h4, p, ul, li, a, label, span { 29 | margin: 0; 30 | padding: 0; 31 | text-decoration: none; 32 | } 33 | li { 34 | list-style-type: none; 35 | } 36 | a { 37 | text-decoration: none; 38 | } 39 | a:hover, a:focus { 40 | text-decoration: underline; 41 | } 42 | /* Grouping content 43 | ========================================================================== */ 44 | /** 45 | * 1. Add the correct box sizing in Firefox. 46 | * 2. Show the overflow in Edge and IE. 47 | */ 48 | hr { 49 | box-sizing: content-box; /* 1 */ 50 | height: 0; /* 1 */ 51 | overflow: visible; /* 2 */ 52 | } 53 | /** 54 | * 1. Correct the inheritance and scaling of font size in all browsers. 55 | * 2. Correct the odd `em` font sizing in all browsers. 56 | */ 57 | pre { 58 | font-family: monospace, monospace; /* 1 */ 59 | font-size: 1em; /* 2 */ 60 | } 61 | /* Text-level semantics 62 | ========================================================================== */ 63 | /** 64 | * 1. Remove the bottom border in Chrome 57- 65 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 66 | */ 67 | abbr[title] { 68 | border-bottom: none; /* 1 */ 69 | text-decoration: underline; /* 2 */ 70 | text-decoration: underline dotted; /* 2 */ 71 | } 72 | /** 73 | * Add the correct font weight in Chrome, Edge, and Safari. 74 | */ 75 | b, 76 | strong { 77 | font-weight: bolder; 78 | } 79 | /** 80 | * 1. Correct the inheritance and scaling of font size in all browsers. 81 | * 2. Correct the odd `em` font sizing in all browsers. 82 | */ 83 | code, 84 | kbd, 85 | samp { 86 | font-family: monospace, monospace; /* 1 */ 87 | font-size: 1em; /* 2 */ 88 | } 89 | /** 90 | * Add the correct font size in all browsers. 91 | */ 92 | small { 93 | font-size: 80%; 94 | } 95 | /** 96 | * Prevent `sub` and `sup` elements from affecting the line height in 97 | * all browsers. 98 | */ 99 | sub, 100 | sup { 101 | font-size: 75%; 102 | line-height: 0; 103 | position: relative; 104 | vertical-align: baseline; 105 | } 106 | sub { 107 | bottom: -0.25em; 108 | } 109 | sup { 110 | top: -0.5em; 111 | } 112 | /* Embedded content 113 | ========================================================================== */ 114 | img { 115 | border-style: none; 116 | display: block; 117 | } 118 | 119 | /* Forms 120 | ========================================================================== */ 121 | /** 122 | * 1. Change the font styles in all browsers. 123 | * 2. Remove the margin in Firefox and Safari. 124 | */ 125 | button, 126 | input, 127 | optgroup, 128 | select, 129 | textarea { 130 | font-family: inherit; /* 1 */ 131 | font-size: 100%; /* 1 */ 132 | line-height: 1.15; /* 1 */ 133 | margin: 0; /* 2 */ 134 | } 135 | /** 136 | * Show the overflow in IE. 137 | * 1. Show the overflow in Edge. 138 | */ 139 | button, 140 | input { 141 | /* 1 */ 142 | overflow: visible; 143 | } 144 | /** 145 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 146 | * 1. Remove the inheritance of text transform in Firefox. 147 | */ 148 | button, 149 | select { 150 | /* 1 */ 151 | text-transform: none; 152 | } 153 | /** 154 | * Correct the inability to style clickable types in iOS and Safari. 155 | */ 156 | button, 157 | [type='button'], 158 | [type='reset'], 159 | [type='submit'] { 160 | -webkit-appearance: button; 161 | } 162 | /** 163 | * Remove the inner border and padding in Firefox. 164 | */ 165 | button::-moz-focus-inner, 166 | [type='button']::-moz-focus-inner, 167 | [type='reset']::-moz-focus-inner, 168 | [type='submit']::-moz-focus-inner { 169 | border-style: none; 170 | padding: 0; 171 | } 172 | /** 173 | * Correct the padding in Firefox. 174 | */ 175 | fieldset { 176 | padding: 0.35em 0.75em 0.625em; 177 | } 178 | /** 179 | * 1. Correct the text wrapping in Edge and IE. 180 | * 2. Remove the padding so developers are not caught out when they zero out 181 | * `fieldset` elements in all browsers. 182 | */ 183 | legend { 184 | box-sizing: border-box; /* 1 */ 185 | display: table; /* 1 */ 186 | max-width: 100%; /* 1 */ 187 | padding: 0; /* 2 */ 188 | white-space: normal; /* 1 */ 189 | } 190 | /** 191 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 192 | */ 193 | progress { 194 | vertical-align: baseline; 195 | } 196 | /** 197 | * Correct the cursor style of increment and decrement buttons in Chrome. 198 | */ 199 | [type='number']::-webkit-inner-spin-button, 200 | [type='number']::-webkit-outer-spin-button { 201 | height: auto; 202 | } 203 | /** 204 | * 1. Correct the odd appearance in Chrome and Safari. 205 | * 2. Correct the outline style in Safari. 206 | */ 207 | [type='search'] { 208 | -webkit-appearance: textfield; /* 1 */ 209 | outline-offset: -2px; /* 2 */ 210 | } 211 | /** 212 | * Remove the inner padding in Chrome and Safari on macOS. 213 | */ 214 | [type='search']::-webkit-search-decoration { 215 | -webkit-appearance: none; 216 | } 217 | /** 218 | * 1. Correct the inability to style clickable types in iOS and Safari. 219 | * 2. Change font properties to `inherit` in Safari. 220 | */ 221 | ::-webkit-file-upload-button { 222 | -webkit-appearance: button; /* 1 */ 223 | font: inherit; /* 2 */ 224 | } 225 | /* Interactive 226 | ========================================================================== */ 227 | /* 228 | * Add the correct display in Edge, IE 10+, and Firefox. 229 | */ 230 | details { 231 | display: block; 232 | } 233 | /* 234 | * Add the correct display in all browsers. 235 | */ 236 | summary { 237 | display: list-item; 238 | } -------------------------------------------------------------------------------- /src/assets/imgs/green-tick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/scss/_base.scss: -------------------------------------------------------------------------------- 1 | @use './variables' as *; // * removes need to call namespace 2 | @use './mixin' as *; // * removes need to call namespace 3 | 4 | // base styles 5 | 6 | body { 7 | min-height: 100vh; 8 | margin: 0; 9 | font-family: $font-default; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | text-align: left; 13 | font-size: $font-size-base; 14 | color: $primary-color; 15 | 16 | //background-color: $primary-color-background; 17 | background: rgb(208,57,135); 18 | background: -moz-linear-gradient(180deg, rgba(208,57,135,1) 0%, rgba(36,74,173,1) 100%); 19 | background: -webkit-linear-gradient(180deg, rgba(208,57,135,1) 0%, rgba(36,74,173,1) 100%); 20 | background: linear-gradient(180deg, rgba(208,57,135,1) 0%, rgba(36,74,173,1) 100%); 21 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#d03987",endColorstr="#244aad",GradientType=1); 22 | } 23 | 24 | code { 25 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 26 | monospace; 27 | } 28 | 29 | h1 { 30 | @include font-template($font-size-large, $font-weight-heavy, $spacing-xlarge !important); 31 | } 32 | 33 | h2 { 34 | @include font-template($font-size-medium, $font-weight-heavy, $spacing-medium !important); 35 | } 36 | 37 | h3 { 38 | @include font-template($font-size-medium, $font-weight-medium, $spacing-medium !important); 39 | } 40 | 41 | p, ul { 42 | font-size: $font-size-standard-extra; 43 | font-weight: $font-weight-light; 44 | line-height: $font-line-height-medium; 45 | 46 | :any-link { 47 | font-size: inherit; 48 | color: $primary-color; 49 | text-decoration: underline; 50 | font-weight: $font-weight-heavy; 51 | } 52 | 53 | } 54 | 55 | //global styles 56 | .spacing-top { 57 | margin-top: $spacing-medium; 58 | } 59 | 60 | .text-center { 61 | text-align: center; 62 | } -------------------------------------------------------------------------------- /src/assets/scss/_buttons.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | @use '../../assets/scss/mixin' as *; // * removes need to call namespace 3 | 4 | .btn-array { 5 | display: flex; 6 | //justify-content: space-between; 7 | //justify-content:space-evenly; 8 | justify-content: space-around; 9 | margin: 0; 10 | 11 | p { 12 | margin: 0 !important; 13 | 14 | input[type="submit"] { 15 | margin: 0; 16 | height: auto; 17 | width: auto; 18 | padding: $spacing-vsmall $spacing-xlarge; 19 | font-weight: $font-weight-light; 20 | text-align: center; 21 | border: none; 22 | border-radius: $border-radius-small; 23 | color: $third-color; // not working! 24 | background-color: $primary-color-background; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | background-color: $primary-color-highlight; 29 | color: $primary-color; 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/scss/_mixin.scss: -------------------------------------------------------------------------------- 1 | @use "./variables" as *; 2 | 3 | // font template 4 | @mixin font-template($f-size, $f-weight, $f-spacing) { 5 | font-size: $f-size; 6 | font-weight: $f-weight; 7 | padding-bottom: $f-spacing; 8 | } -------------------------------------------------------------------------------- /src/assets/scss/_progress-bar.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | @use '../../assets/scss/mixin' as *; // * removes need to call namespace 3 | 4 | // Progressbar 5 | .progressbar { 6 | position: relative; 7 | display: flex; 8 | justify-content: space-between; 9 | counter-reset: step; 10 | margin: $spacing-large 0; 11 | 12 | // in-active circles 13 | .progress-step { 14 | width: 2.5rem; 15 | height: 2.5rem; 16 | background-color: $third-color; 17 | border-radius: 50%; 18 | border: $border-width-large solid $primary-color; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | color: $primary-color; 23 | z-index: 2; 24 | } 25 | 26 | // circle 27 | .progress-step::before { 28 | counter-increment: step; 29 | content: counter(step); 30 | //color: $primary-color; 31 | } 32 | 33 | // top text 34 | .progress-step::after { 35 | content: attr(data-title); 36 | position: absolute; 37 | top: calc(-100% + $spacing-medium); 38 | font-size: $font-size-small; 39 | color: $primary-color; 40 | font-weight: $font-weight-heavy; 41 | } 42 | 43 | // active circle 44 | .progress-step-active { 45 | background-color: $primary-color-background; 46 | color: $third-color; 47 | border: none; 48 | } 49 | 50 | } 51 | 52 | // progress bar line 53 | .progressbar::before, 54 | .progress { 55 | content: ""; 56 | position: absolute; 57 | top: 50%; 58 | transform: translateY(-50%); 59 | height: 3px; 60 | width: 100%; 61 | background-color: $primary-color; 62 | z-index: 1; 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=swap'); 2 | 3 | // font 4 | $font-default: 'Roboto', sans-serif; 5 | //$font-default: Montserrat, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 6 | //$font-default: Helvetica, sans-serif, Arial; 7 | 8 | // font sizes 9 | $font-size-base: 16px; 10 | $font-size-small: 0.85rem; 11 | $font-size-standard: 1rem; 12 | $font-size-standard-extra: 1.1rem; 13 | $font-size-medium: 1.3rem; // h2 14 | $font-size-large: 2rem; // h1 15 | 16 | // font line heights 17 | $font-line-height-default: normal; 18 | $font-line-height-small: 1.2rem; 19 | $font-line-height-medium: 1.4rem; 20 | $font-line-height-large: 2rem; 21 | 22 | // font weights 23 | $font-weight-none: normal; 24 | $font-weight-light: 100; 25 | $font-weight-medium: 500; 26 | $font-weight-heavy: 700; 27 | $font-weight-heavyplus: 900; 28 | 29 | 30 | // spacings 31 | $spacing-vsmall: 0.5rem; 32 | $spacing-small: 0.75rem; 33 | $spacing-medium: 1rem; 34 | $spacing-large: 1.5rem; 35 | $spacing-xlarge: 2rem; 36 | 37 | 38 | // colors 39 | $primary-color: #000; 40 | $primary-color-highlight: yellow; 41 | $primary-color-background: #D03987; 42 | $secondary-color: #ccc; 43 | $third-color: #fff; 44 | $color-success: green; 45 | $color-alert: red; 46 | 47 | 48 | // borders 49 | $border-width-small: 1px; 50 | $border-width-medium: 2px; 51 | $border-width-large: 3px; 52 | $border-radius-small: 0.25rem; 53 | $border-radius-medium: 0.5rem; 54 | $border-radius-large: 1rem; 55 | 56 | // breakpoints 57 | $breakpoint1: 630px; -------------------------------------------------------------------------------- /src/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use './base'; -------------------------------------------------------------------------------- /src/components/form-completed/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import IMGgreentick from '../../assets/imgs/green-tick.svg'; // load image 4 | import './styles.scss'; 5 | 6 | function FormUserResult({ pageTitle, successMessage }) { 7 | 8 | // Get Redux Form State and output to JSON format 9 | const state = useSelector(state => state) 10 | const stateOutput = (`JSON Data Form-Completed: ${JSON.stringify(state, null, 2)}`) 11 | console.log(stateOutput) // output to console.log 12 | 13 | return ( 14 | 15 | <> 16 | 17 |
18 | 19 |

{pageTitle || 'Confirmation'}

20 | 21 | {successMessage 26 | 27 |

28 | {successMessage || 'Thank you, please check your email!'} 29 |

30 | 31 |
32 | 33 |
34 |
{stateOutput}
35 |
36 | 37 | 38 | 39 | ); 40 | 41 | } 42 | 43 | export default FormUserResult; 44 | -------------------------------------------------------------------------------- /src/components/form-completed/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | 3 | .form-complete { 4 | 5 | h2 { 6 | color: $color-success; 7 | text-align: center; 8 | } 9 | 10 | img { 11 | margin: 0 auto; 12 | padding: $spacing-medium 0 $spacing-large 0; 13 | width: 100px; 14 | height: auto; 15 | } 16 | 17 | // fade in image transition 18 | .fade-in-image { 19 | animation: fadeIn 2.5s; 20 | -webkit-animation: fadeIn 2.5s; 21 | -moz-animation: fadeIn 2.5s; 22 | -o-animation: fadeIn 2.5s; 23 | -ms-animation: fadeIn 2.5s; 24 | } 25 | @keyframes fadeIn { 26 | 0% {opacity:0;} 27 | 100% {opacity:1;} 28 | } 29 | 30 | p { 31 | text-align: center; 32 | color: $color-success; 33 | font-weight: $font-weight-medium; 34 | line-height: $font-line-height-large; 35 | } 36 | 37 | } 38 | 39 | // JSON page code outout render 40 | .code-output { 41 | margin-top: $spacing-xlarge; 42 | border-radius: $border-radius-medium; 43 | background-color: $secondary-color; 44 | 45 | // code format output 46 | pre { 47 | font-weight: $font-weight-medium; 48 | padding: $spacing-medium; 49 | color: $primary-color-background; 50 | white-space: pre-wrap; // break words that are too long 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/components/form-privacy/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { formStage, formPrivacy } from '../../store/rootSlice' 4 | import './styles.scss'; 5 | 6 | function FormUserPrivacy({ pageTitle, submitButtonText, previousButton }) { 7 | 8 | // redux 9 | const dispatch = useDispatch(); 10 | 11 | // get Redux store values for formUserPrivacy 12 | const currentStage = useSelector(state => state.FormStage) // for previous button 13 | const stateSignup1 = useSelector(state => state.FormUserPrivacy.signup1) 14 | const stateSignup2 = useSelector(state => state.FormUserPrivacy.signup2) 15 | 16 | const state = useSelector(state => state) 17 | const stateOutput = (`JSON Data Form-Privacy: ${JSON.stringify(state, null, 2)}`) 18 | //console.log(stateOutput) // output to console.log 19 | 20 | // toggle checkboxes onchange 21 | const [isChecked1, setIsChecked1] = useState(stateSignup1 || false); // from redux initial state or form 22 | const [isChecked2, setIsChecked2] = useState(stateSignup2 || false); // from redux initial state or form 23 | const handleChange1 = (e) => { 24 | setIsChecked1(!isChecked1); 25 | } 26 | const handleChange2 = (e) => { 27 | setIsChecked2(!isChecked2); 28 | } 29 | 30 | // onsubmit 31 | const [isSubmitted, setIsSubmitted] = useState(false) // state for form status 32 | const handleSubmit = (e) => { 33 | e.preventDefault(); // stop form submission 34 | setIsSubmitted(true) // update form status to submitted 35 | } 36 | 37 | useEffect(() => { 38 | if (isSubmitted) { // check if form status submitted 39 | 40 | // update Redux Store Slice 41 | dispatch( 42 | formStage(3) // update formStage and move to next stage 43 | ) 44 | dispatch( 45 | formPrivacy({ 46 | signup1: isChecked1, // update form checkbox status 47 | signup2: isChecked2 48 | }) 49 | ); 50 | 51 | } 52 | 53 | }, [isSubmitted, dispatch, stateOutput, isChecked1, isChecked2]) 54 | 55 | return ( 56 | 57 | <> 58 |

{pageTitle || 'Privacy'}

59 | 60 |
65 | 66 |

67 | 74 | 75 |

76 | 77 |

78 | 85 | 86 |

87 | 88 |
89 | {(previousButton) && 90 |

91 | dispatch(formStage(currentStage-1))} 95 | /> 96 |

97 | } 98 |

99 | 103 |

104 |
105 | 106 |
107 | 108 | 109 | 110 | ); 111 | 112 | } 113 | 114 | export default FormUserPrivacy; 115 | -------------------------------------------------------------------------------- /src/components/form-privacy/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | @use '../../assets/scss/buttons' as *; // * removes need to call namespace 3 | 4 | form#form-privacy { 5 | margin-top: $spacing-medium; 6 | 7 | p { 8 | &.form-boxes { 9 | padding-bottom: $spacing-medium; 10 | } 11 | } 12 | 13 | label { 14 | font-size: $font-size-standard; 15 | font-weight: $font-weight-heavy; 16 | position: relative; 17 | top: -$spacing-vsmall; 18 | } 19 | 20 | input[type="checkbox"] { // target checkboxes 21 | margin: 0 $spacing-medium $spacing-medium 0; 22 | width: $spacing-large; // check box size w 23 | height: $spacing-large; // check box size h 24 | background-color: $primary-color-highlight; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/components/form-signup/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { formStage, formSignup } from '../../store/rootSlice' 4 | import './styles.scss'; 5 | 6 | function FormUserSignup({ pageTitle, submitButtonText, previousButton }) { 7 | 8 | // redux 9 | const dispatch = useDispatch(); 10 | 11 | // get Redux store values for formUserSignup 12 | const currentStage = useSelector(state => state.FormStage) // for previous button 13 | const formstageName = useSelector(state => state.FormUserSignup.name) 14 | const formstageRole = useSelector(state => state.FormUserSignup.role) 15 | const formstageEmail = useSelector(state => state.FormUserSignup.email) 16 | const formstagePass = useSelector(state => state.FormUserSignup.password) 17 | 18 | // form values initial state 19 | const [formData, setFormData] = useState({ 20 | name: formstageName || "", 21 | role: formstageRole || "", 22 | email: formstageEmail || "", 23 | password: formstagePass || "", 24 | }) 25 | 26 | // form values onchange 27 | const handleChange = (e) => { 28 | const { name, value } = e.target 29 | setFormData({ 30 | ...formData, 31 | [name]: value 32 | }) 33 | } 34 | 35 | // form validation checks 36 | const [errors, setErrors] = useState({}) 37 | const validate = (formData) => { 38 | 39 | let formErrors = {} // set form errors to none at start 40 | 41 | // name 42 | if(!formData.name){ 43 | formErrors.name = "Name required"; 44 | } 45 | 46 | // email 47 | const emailRegex = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i); 48 | if(!formData.email || !emailRegex.test(formData.email)) { 49 | formErrors.email = 'Valid Email required'; 50 | } 51 | 52 | // password 53 | const passwordRegex = new RegExp('(?=.*[a-z])+(?=.*[A-Z])+(?=.*[0-9])+(?=.{10,})') 54 | if(!formData.password || !passwordRegex.test(formData.password)) { 55 | formErrors.password = 'The minimum password length is 10 characters and must contain at least 1 lowercase letter, 1 uppercase letter and 1 number)'; 56 | //console.log(formData.password.length) 57 | } 58 | 59 | return formErrors 60 | } 61 | 62 | const [isSubmitted, setIsSubmitted] = useState(false) // state for sent status 63 | // onsubmit 64 | const handleSubmit = (e) => { 65 | e.preventDefault(); // stop form submission 66 | setErrors(validate(formData)) // check errors 67 | setIsSubmitted(true) // update submit status 68 | } 69 | 70 | useEffect(() => { 71 | if (Object.keys(errors).length === 0 && isSubmitted) { // check if any form errors 72 | 73 | // update Redux Slice 74 | dispatch( 75 | formStage(2) // update formStage 76 | ) 77 | dispatch( 78 | formSignup({ // update formSignup 79 | name: formData.name, 80 | role: formData.role, 81 | email: formData.email, 82 | password: formData.password 83 | }) 84 | ); 85 | } 86 | 87 | }, [formData, isSubmitted, dispatch, errors]) 88 | // console.log(errors, formData) 89 | 90 | return ( 91 | 92 | <> 93 |

{pageTitle || 'Signup'}

94 | 95 |
handleSubmit(e)} 99 | > 100 | 101 |

102 | 103 | 114 |

115 | {errors.name && {errors.name}} 116 | 117 |

118 | 119 | 130 |

131 | 132 |

133 | 134 | 145 |

146 | {errors.email && {errors.email}} 147 | 148 |

149 | 150 | 160 |

161 | {errors.password && {errors.password}} 162 | 163 |

* required fields

164 | 165 |
166 | {(previousButton) && 167 |

168 | dispatch(formStage(currentStage-1))} 172 | /> 173 |

174 | } 175 |

176 | 180 |

181 |
182 | 183 |
184 | 185 | 186 | 187 | ); 188 | 189 | } 190 | 191 | export default FormUserSignup; 192 | -------------------------------------------------------------------------------- /src/components/form-signup/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | @use '../../assets/scss/buttons' as *; // * removes need to call namespace 3 | 4 | form#form-signup { 5 | 6 | p { 7 | margin: $spacing-medium 0; 8 | display: block; 9 | } 10 | 11 | label { 12 | font-size: $font-size-standard; 13 | margin-right: $spacing-vsmall; 14 | font-weight: $font-weight-heavy; 15 | } 16 | 17 | input:not([type="submit"]){ // target all inputs except submit button 18 | width: 100%; 19 | margin-top: $spacing-vsmall; // remove?? 20 | flex: 1; 21 | padding: $spacing-vsmall; 22 | border: $border-width-large solid $primary-color; 23 | 24 | &::placeholder { 25 | color: $secondary-color; 26 | } 27 | 28 | &:hover { 29 | background-color: $primary-color-highlight; 30 | } 31 | 32 | } 33 | 34 | .required-asterix { 35 | color: $primary-color; 36 | padding-left: $spacing-vsmall; 37 | } 38 | 39 | .disclaimer-text { 40 | font-size: $font-size-standard; 41 | } 42 | 43 | // for validation error text 44 | .error-message { 45 | color: $color-alert; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import { Provider } from "react-redux"; 5 | import { store } from './store' 6 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 7 | import './assets/scss/main.scss'; 8 | import './assets/css/normalize.css'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | 19 | // If you want your app to work offline and load faster, you can change 20 | // unregister() to register() below. Note this comes with some pitfalls. 21 | // Learn more about service workers: https://cra.link/PWA 22 | serviceWorkerRegistration.register(); 23 | -------------------------------------------------------------------------------- /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'; 6 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import { reducer } from './rootSlice' 3 | 4 | export const store = configureStore({ 5 | reducer 6 | }) 7 | -------------------------------------------------------------------------------- /src/store/rootSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | // Slice 4 | const rootSlice = createSlice({ 5 | 6 | name: "root", 7 | 8 | initialState: { 9 | FormStage: 1, // default page stage to show on page load 10 | FormUserSignup: "", 11 | FormUserPrivacy: "" 12 | }, 13 | 14 | reducers: { 15 | formStage: (state, action) => { state.FormStage = action.payload }, 16 | formSignup: (state, action) => { state.FormUserSignup = action.payload }, 17 | formPrivacy: (state, action) => { state.FormUserPrivacy = action.payload } 18 | } 19 | 20 | }) 21 | 22 | // Actions 23 | export const { formStage, formSignup, formPrivacy } = rootSlice.actions 24 | export const reducer = rootSlice.reducer; 25 | -------------------------------------------------------------------------------- /src/views/signup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux' 3 | import LazyLoad from 'react-lazyload'; // use lazyload for components and image 4 | import FormUserSignup from '../../components/form-signup'; // load component 5 | import FormUserPrivacy from '../../components/form-privacy'; // load component 6 | import FormUserCompleted from '../../components/form-completed'; // load component 7 | import './styles.scss'; 8 | 9 | const Signup = () => { 10 | 11 | const pageStage = useSelector(state => state.FormStage) 12 | //const stateAll = useSelector(state => state) 13 | //console.log(`output: ${JSON.stringify(stateAll, null, 2)}`) // output results to console.log 14 | 15 | return ( 16 | 17 |
18 |
19 | 20 |

24 | Signup Form 25 |

26 | 27 |
28 | 29 | {/* When adding/removing components, update the progress bar below */} 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 | {(pageStage === 1) && 41 | // Signup Page 42 | 43 |
44 | 49 |
50 |
51 | } 52 | 53 | {(pageStage === 2) && 54 | // Privacy Page 55 | 56 |
57 | 62 |
63 |
64 | } 65 | 66 | {(pageStage === 3) && 67 | // Completion Page 68 | 69 |
70 | 74 |
75 |
76 | } 77 | 78 |
79 | 80 |
81 |
82 |
83 | 84 | ); 85 | 86 | }; 87 | 88 | export default Signup; 89 | -------------------------------------------------------------------------------- /src/views/signup/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../../assets/scss/variables' as *; // * removes need to call namespace 2 | @use '../../assets/scss/mixin' as *; // * removes need to call namespace 3 | @use '../../assets/scss/progress-bar' as *; // * removes need to call namespace 4 | 5 | // page content wrapper 6 | main { 7 | padding-top: $spacing-vsmall; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | .form-wrapper { 13 | margin: $spacing-small $spacing-medium $spacing-medium $spacing-medium; 14 | padding: $spacing-medium $spacing-xlarge; 15 | background-color: $third-color; 16 | border-radius: $border-radius-large; 17 | box-shadow: 2px 2px 10px $primary-color; 18 | width: clamp(320px, 50%, $breakpoint1); 19 | } 20 | 21 | } 22 | --------------------------------------------------------------------------------