├── .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 | 
43 |
44 |
45 | ## Components
46 |
47 | ### Form User Signup Component
48 | Component for Signup Page
49 | 
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 | 
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 | 
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 | 
88 | 
89 |
90 |
91 | ## Other Testing
92 |
93 | __Google Lighthouse__
94 |
95 | 
96 |
97 | __[Accessiblity Testing Link](https://wave.webaim.org/report#/https://unruffled-mcnulty-71b799.netlify.app/)__
98 | 
99 |
100 | __[CSS Testing Link](https://jigsaw.w3.org/css-validator/validator?profile=css3&warning=0&uri=https://unruffled-mcnulty-71b799.netlify.app/)__
101 | 
102 |
103 | __[HTML Testing Link](https://validator.w3.org/nu/?doc=https://unruffled-mcnulty-71b799.netlify.app/)__
104 | 
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 |
26 |
27 |
28 | {successMessage || 'Thank you, please check your email!'}
29 |
30 |
31 |
32 |
33 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------