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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------