Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contact page fully functional. Utilizes Material UI design components and inherits styles from theme #1330

Merged
merged 38 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f3c8cc9
added sendContactData function and its entry into rootSaga in /client…
edwinjue Aug 23, 2022
2f6f6d2
refactored /client/components/main/ContactForm.jsx from class to func…
edwinjue Aug 23, 2022
025e3f7
added boilerplate scss files from v1 into client/styles for ContactFo…
edwinjue Aug 24, 2022
12782ae
moved all contact files under client/components/contact, added Button…
edwinjue Aug 25, 2022
43db4cf
initial refactor of contact form utilizing material-ui
edwinjue Aug 25, 2022
5821a6c
brought back the contact image
edwinjue Aug 25, 2022
7fb307d
applied form validation control to material-ui TextField components. …
edwinjue Aug 25, 2022
f3a73eb
added Grid component to ContactIntro. contact page is looking present…
edwinjue Aug 25, 2022
216db4b
contact form now shows a confirmation message upon successful submiss…
edwinjue Aug 26, 2022
7df6c06
reconfigured ToastContainer in Contact.jsx and toast calls in Contact…
edwinjue Aug 26, 2022
dbdb492
removed some blank lines to improve readability
edwinjue Aug 26, 2022
4c28f42
turned off autocomplete for all MUI TextField components in the Conta…
edwinjue Aug 26, 2022
dca1d27
display mui CircularProgress element when submit button clicked to di…
edwinjue Aug 26, 2022
b2c08e0
added a few comments and reorganized code in ContactForm.jsx to impro…
edwinjue Aug 26, 2022
2bb011f
added components/contact/styles/
edwinjue Aug 26, 2022
1ed2871
removed some unused files and comments
edwinjue Aug 26, 2022
d9c09e8
tiny formatting change
edwinjue Aug 26, 2022
799bf10
updated contactForm code per es-lint. added webpack.*.js to .eslintig…
edwinjue Aug 28, 2022
1c80214
replaced Grid justify property with justifyContent
edwinjue Aug 28, 2022
13f76a4
made some changes as per review comments
edwinjue Aug 30, 2022
0df22a7
made some more changes as per review comments
edwinjue Aug 30, 2022
07530cf
made some more changes as per review comments
edwinjue Aug 30, 2022
3414da3
removed a console.log in ContactForm
edwinjue Aug 30, 2022
437c9d2
refactored client/components/contact/ContactImage to make use of make…
edwinjue Aug 31, 2022
f6a54fe
removed explicit comparison to true in ternary operator used by Circu…
edwinjue Aug 31, 2022
95d2a0d
in conditional statements, removed use of type coercion on variables …
edwinjue Sep 1, 2022
8a03971
added early return in onSubmit handler to return false if validation …
edwinjue Sep 1, 2022
ad333f6
updated syntax of early return in onSubmit handler
edwinjue Sep 1, 2022
435d4a4
got rid of methods to dispatch redux actions defined on top and calle…
edwinjue Sep 1, 2022
60bcb80
removed explicit comparison to false in ternary operator used by Button
edwinjue Sep 1, 2022
b13a1d3
tested contact form submission offline. made necessary updates to ens…
edwinjue Sep 2, 2022
0e80c9f
removed comment // response: { status }
edwinjue Sep 3, 2022
ceb9a77
updated fix to metadata.js status is undefined console error
edwinjue Sep 3, 2022
4c2c5c2
removed contact/settings until contact form utilizes more configurati…
edwinjue Sep 3, 2022
aa82a82
reverted /client/redux/reducers/metadata.js and /client/redux/sagas/m…
edwinjue Sep 3, 2022
c1492a0
moved toast to bottom-right
edwinjue Sep 7, 2022
85be5b0
Merge branch 'dev' into 1176-auto-create-issues-of-contact-us-form-me…
edwinjue Sep 7, 2022
95327a6
eslint
edwinjue Sep 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
*.test.js*
v1/*
dist/*

webpack.*.js
4 changes: 2 additions & 2 deletions client/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Privacy from '@components/main/Privacy';
import Faqs from '@components/main/Faqs';
import About from '@components/main/About';
import Blog from '@components/main/Blog';
import ContactForm from '@components/main/ContactForm';
import Contact from '@components/contact/Contact';

export default function Routes() {
const { pathname } = useLocation();
Expand All @@ -28,7 +28,7 @@ export default function Routes() {
<Route path="/faqs" component={Faqs} />
<Route path="/about" component={About} />
<Route path="/blog" component={Blog} />
<Route path="/contact" component={ContactForm} />
<Route path="/contact" component={Contact} />
<Route path="/">
<Redirect to="map" />
</Route>
Expand Down
32 changes: 32 additions & 0 deletions client/components/contact/Contact.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { ToastContainer } from 'react-toastify';
import ContactImage from './ContactImage';
import ContactIntro from './ContactIntro';
import ContactForm from './ContactForm';

import 'react-toastify/dist/ReactToastify.css';

const Contact = () => (
<>
<div>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</div>
<div>
<ContactImage>Contact Us</ContactImage>
<ContactIntro />
<ContactForm />
</div>
</>
);

export default Contact;
270 changes: 270 additions & 0 deletions client/components/contact/ContactForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { sendGitRequest } from '@reducers/data';
import { showFeedbackSuccess, setErrorModal } from '@reducers/ui';
import { toast } from 'react-toastify';

import 'react-toastify/dist/ReactToastify.css';

import {
Container,
Grid,
Button,
TextField,
CircularProgress,
} from '@material-ui/core';

const initialFormValues = {
firstName: '',
lastName: '',
email: '',
association: '',
message: '',
errors: {
missingFirstName: false,
missingLastName: false,
missingEmail: false,
invalidEmail: false,
missingMessage: false,
},
loading: false,
};

const ContactForm = () => {
// define the methods to dispatch redux actions
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
const dispatch = useDispatch();
const callSendGitRequest = useCallback(obj => dispatch(sendGitRequest(obj)), [dispatch]);
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
const callShowFeedbackSuccess = useCallback(o => dispatch(showFeedbackSuccess(o)), [dispatch]);
const callShowErrorModal = useCallback(obj => dispatch(setErrorModal(obj)), [dispatch]);

// mapStateToProps equivalent
const displayFeedbackSuccess = useSelector(state => state.ui.displayFeedbackSuccess);
const openErrorModal = useSelector(state => state.ui.error.isOpen);

const [formValues, setFormValues] = useState(initialFormValues);

function clearFields() {
setFormValues({
...initialFormValues,
});
}

// initialize component
useEffect(() => {
// componentDidMount code goes here...
clearFields();
if (!!displayFeedbackSuccess === true) {
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
toast.success('We received your message. Our team will contact you at the email address provided.', {
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}

if (!!openErrorModal === true) {
toast.error('We failed to process your message. Please try again later.', {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}

return () => {
// componentWillUnmount code goes here...
callShowFeedbackSuccess(false);
callShowErrorModal(false);
clearFields();
};
}, [callShowErrorModal, callShowFeedbackSuccess, displayFeedbackSuccess, openErrorModal]);

// helper methods
function validateEmail(emailAddress) {
// eslint-disable-next-line
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(emailAddress)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just directly return the value from test here?

Also would be nice to make a const for this regex expression and have a comment explaining what it's checking for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great idea. thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in latest push

return true;
}
return false;
}

const clearErrors = useCallback(() => {
setFormValues(prevState => ({
...prevState,
...{
errors: {
missingFirstName: false,
missingLastName: false,
missingEmail: false,
invalidEmail: false,
missingMessage: false,
},
},
}));
}, []);

const validateForm = useCallback(() => {
const noFirstName = formValues.firstName.trim().length === 0;
const noLastName = formValues.lastName.trim().length === 0;
const noEmail = formValues.email.trim().length === 0;
const noMessage = formValues.message.trim().length === 0;
const incompleteEmail = (!noEmail && !validateEmail(formValues.email));
if (!noFirstName && !noLastName && !noEmail && !noMessage && !incompleteEmail) {
return true;
}

setFormValues(prevState => ({
...prevState,
...{
errors: {
missingFirstName: noFirstName,
missingLastName: noLastName,
missingEmail: noEmail,
invalidEmail: incompleteEmail,
missingMessage: noMessage,
},
},
}));
return false;
}, [formValues]);

// event handlers
const onInputChange = useCallback(event => {
event.preventDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding, can you explain what this line does?

Copy link
Member Author

@edwinjue edwinjue Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.preventDefault() prevents the default event behavior from triggering if it is cancellable. just had it in here to be safe to prevent any conflicts from happening between the event's native behavior and react but I just learned that onChange isn't cancellable so this line has no effect and I will remove it

const { name, value } = event.target;
setFormValues(prevState => ({ ...prevState, [name]: value }));
}, []);

const handleSubmit = useCallback(event => {
event.preventDefault();

if (validateForm()) {
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
const body = [
`First name: ${formValues.firstName}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to use the trimmed versions here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right. i know we're not making any system calls with the input but do you think we need to do any other sort of further input validation/sanitization? like prevent or scrub out "bad characters" like ";", "<" or "&"

`Last name: ${formValues.lastName}`,
`Email: ${formValues.email}`,
`Association: ${formValues.association || 'Not provided'}`,
`Message: ${formValues.message}`,
].join('\n');

setFormValues(prevState => ({
...prevState,
...{
loading: true,
},
}));

// dispatch action to redux with payload
callSendGitRequest({ title: formValues.email, body });
}
}, [callSendGitRequest,
formValues.association,
formValues.email,
formValues.firstName,
formValues.lastName,
formValues.message,
validateForm]);

return (
<Container maxWidth="sm">
<form id="contact-form" onSubmit={handleSubmit}>
<Grid container alignItems="center" justify="center" direction="column" style={{ gap: '10px' }}>
<Grid container alignItems="center" justify="center" direction="row" spacing={2}>
<Grid item xs={6}>
<TextField
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we have multiple TextFields with some subset of the same properties. Can we make a subclass or something of that sort so that we don't need to repeat all those properties?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to but I'm not sure if we can without creating our own TextField component that takes in an object that we can set as a prop but I think doing so would add more complexity to just using material-ui components as they are. However, if you or anyone else could suggest a way we could minimize repetitive code here, I'll be interested in trying it out

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jekijo or Jordan, any ideas?

id="contact-firstname"
name="firstName"
label="First Name *"
type="text"
autoComplete="off"
value={formValues.firstName}
onChange={onInputChange}
onFocus={clearErrors}
error={formValues.errors.missingFirstName}
helperText={formValues.errors.missingFirstName ? 'Please provide a first name.' : ''}
fullWidth
/>
</Grid>
<Grid item xs={6}>
<TextField
id="contact-lastname"
name="lastName"
label="Last Name *"
type="text"
autoComplete="off"
value={formValues.lastName}
onChange={onInputChange}
onFocus={clearErrors}
error={formValues.errors.missingLastName}
helperText={formValues.errors.missingLastName ? 'Please provide a last name.' : ''}
fullWidth
/>
</Grid>
</Grid>
<Grid container alignItems="center" justify="center" direction="row">
<Grid item xs={12}>
<TextField
id="contact-email"
name="email"
label="Email *"
type="text"
autoComplete="off"
value={formValues.email}
onChange={onInputChange}
onFocus={clearErrors}
error={formValues.errors.missingEmail || formValues.errors.invalidEmail}
helperText={formValues.errors.missingEmail || formValues.errors.invalidEmail ? 'Please provide a valid email address.' : ''}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<TextField
id="contact-association"
name="association"
label="Association"
type="text"
autoComplete="off"
value={formValues.association}
onChange={onInputChange}
fullWidth
/>
</Grid>
<Grid item xs={12} style={{ paddingTop: '8px' }}>
<TextField
id="contact-message"
name="message"
label="Message *"
type="text"
variant="outlined"
rows={4}
autoComplete="off"
value={formValues.message}
onChange={onInputChange}
onFocus={clearErrors}
error={formValues.errors.missingMessage}
helperText={formValues.errors.missingMessage ? 'Please provide a message.' : ''}
fullWidth
multiline
/>
</Grid>
</Grid>
<Grid container direction="column" alignItems="center" justify="center" style={{ paddingTop: '8px' }}>
<CircularProgress style={{ display: formValues.loading === true ? 'block' : 'none' }} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove === true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in the latest version

<Button variant="contained" color="primary" type="submit" style={{ display: formValues.loading === false ? 'block' : 'none' }}>
Submit
</Button>
</Grid>
</Grid>
</form>
</Container>

);
};

export default ContactForm;
17 changes: 17 additions & 0 deletions client/components/contact/ContactImage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import PropTypes from 'prop-types';
import React from 'react';
import './styles/contactimage.scss';

const ContactImage = ({ children }) => (
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
<div className="contact-image-cover">
<div className="contact-image-overlay-text">
<span className="contact-image-text-contact">{children}</span>
</div>
</div>
);

ContactImage.propTypes = {
children: PropTypes.node.isRequired,
};

export default ContactImage;
22 changes: 22 additions & 0 deletions client/components/contact/ContactIntro.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import {
Grid,
} from '@material-ui/core';

const ContactIntro = () => (
<Grid container alignItems="center" justify="center" direction="column">
<Grid item style={{ width: '45%' }}>
<h1 style={{ textAlign: 'center' }}>
Don&apos;t See What You Need?
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
</h1>
<p>
We want to build a tool that works for you. We are open to suggestions and
feedback and would love the opportunity to get connected. Feel free to input
your information in the contact form below and we will be sure to get back to
you within 2-3 business days. Thank you!
</p>
</Grid>
</Grid>
);

export default ContactIntro;
20 changes: 20 additions & 0 deletions client/components/contact/styles/contactimage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

.contact-image-cover {
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
height: 25vh;
background-image: url('../../../assets/contact_bg.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
position: relative;

.contact-image-overlay-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 40px;
font-weight: bold;
edwinjue marked this conversation as resolved.
Show resolved Hide resolved
}
}

Loading