-
-
Notifications
You must be signed in to change notification settings - Fork 64
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
edwinjue
merged 38 commits into
dev
from
1176-auto-create-issues-of-contact-us-form-messages
Sep 7, 2022
Merged
Contact page fully functional. Utilizes Material UI design components and inherits styles from theme #1330
Changes from 30 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 2f6f6d2
refactored /client/components/main/ContactForm.jsx from class to func…
edwinjue 025e3f7
added boilerplate scss files from v1 into client/styles for ContactFo…
edwinjue 12782ae
moved all contact files under client/components/contact, added Button…
edwinjue 43db4cf
initial refactor of contact form utilizing material-ui
edwinjue 5821a6c
brought back the contact image
edwinjue 7fb307d
applied form validation control to material-ui TextField components. …
edwinjue f3a73eb
added Grid component to ContactIntro. contact page is looking present…
edwinjue 216db4b
contact form now shows a confirmation message upon successful submiss…
edwinjue 7df6c06
reconfigured ToastContainer in Contact.jsx and toast calls in Contact…
edwinjue dbdb492
removed some blank lines to improve readability
edwinjue 4c28f42
turned off autocomplete for all MUI TextField components in the Conta…
edwinjue dca1d27
display mui CircularProgress element when submit button clicked to di…
edwinjue b2c08e0
added a few comments and reorganized code in ContactForm.jsx to impro…
edwinjue 2bb011f
added components/contact/styles/
edwinjue 1ed2871
removed some unused files and comments
edwinjue d9c09e8
tiny formatting change
edwinjue 799bf10
updated contactForm code per es-lint. added webpack.*.js to .eslintig…
edwinjue 1c80214
replaced Grid justify property with justifyContent
edwinjue 13f76a4
made some changes as per review comments
edwinjue 0df22a7
made some more changes as per review comments
edwinjue 07530cf
made some more changes as per review comments
edwinjue 3414da3
removed a console.log in ContactForm
edwinjue 437c9d2
refactored client/components/contact/ContactImage to make use of make…
edwinjue f6a54fe
removed explicit comparison to true in ternary operator used by Circu…
edwinjue 95d2a0d
in conditional statements, removed use of type coercion on variables …
edwinjue 8a03971
added early return in onSubmit handler to return false if validation …
edwinjue ad333f6
updated syntax of early return in onSubmit handler
edwinjue 435d4a4
got rid of methods to dispatch redux actions defined on top and calle…
edwinjue 60bcb80
removed explicit comparison to false in ternary operator used by Button
edwinjue b13a1d3
tested contact form submission offline. made necessary updates to ens…
edwinjue 0e80c9f
removed comment // response: { status }
edwinjue ceb9a77
updated fix to metadata.js status is undefined console error
edwinjue 4c2c5c2
removed contact/settings until contact form utilizes more configurati…
edwinjue aa82a82
reverted /client/redux/reducers/metadata.js and /client/redux/sagas/m…
edwinjue c1492a0
moved toast to bottom-right
edwinjue 85be5b0
Merge branch 'dev' into 1176-auto-create-issues-of-contact-us-form-me…
edwinjue 95327a6
eslint
edwinjue File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,4 @@ | |
*.test.js* | ||
v1/* | ||
dist/* | ||
|
||
webpack.*.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
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 { | ||
Container, | ||
Grid, | ||
Button, | ||
TextField, | ||
CircularProgress, | ||
} from '@material-ui/core'; | ||
import contactSettings from './settings'; | ||
import 'react-toastify/dist/ReactToastify.css'; | ||
|
||
const initialFormValues = { | ||
firstName: '', | ||
lastName: '', | ||
email: '', | ||
association: '', | ||
message: '', | ||
errors: { | ||
missingFirstName: false, | ||
missingLastName: false, | ||
missingEmail: false, | ||
invalidEmail: false, | ||
missingMessage: false, | ||
}, | ||
loading: false, | ||
}; | ||
|
||
const ContactForm = () => { | ||
const dispatch = useDispatch(); | ||
|
||
// 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) { | ||
toast.success('We received your message. Our team will contact you at the email address provided.', contactSettings.toast.dark); | ||
} | ||
|
||
if (openErrorModal === true) { | ||
toast.error('We failed to process your message. Please try again later.', contactSettings.toast.dark); | ||
} | ||
|
||
return () => { | ||
// componentWillUnmount code goes here... | ||
dispatch(showFeedbackSuccess(false)); | ||
dispatch(setErrorModal(false)); | ||
clearFields(); | ||
}; | ||
}, [dispatch, displayFeedbackSuccess, openErrorModal]); | ||
|
||
// Helper methods. | ||
function validateEmail(emailAddress) { | ||
// A regular expression checking for a valid email format. | ||
const VALID_EMAIL_FORMAT_REGEX = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; | ||
return VALID_EMAIL_FORMAT_REGEX.test(emailAddress); | ||
} | ||
|
||
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 => { | ||
const { name, value } = event.target; | ||
setFormValues(prevState => ({ ...prevState, [name]: value })); | ||
}, []); | ||
|
||
const handleSubmit = useCallback(event => { | ||
event.preventDefault(); | ||
|
||
if (!validateForm()) { | ||
return; | ||
} | ||
|
||
const body = [ | ||
`First name: ${formValues.firstName.trim()}`, | ||
`Last name: ${formValues.lastName.trim()}`, | ||
`Email: ${formValues.email.trim()}`, | ||
`Association: ${formValues.association.trim() || 'Not provided'}`, | ||
`Message: ${formValues.message.trim()}`, | ||
].join('\n'); | ||
|
||
setFormValues(prevState => ({ | ||
...prevState, | ||
...{ | ||
loading: true, | ||
}, | ||
})); | ||
|
||
// Dispatch action to redux with payload. | ||
dispatch(sendGitRequest({ title: formValues.email, body })); | ||
}, [dispatch, | ||
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" justifyContent="center" direction="column" style={{ gap: '10px' }}> | ||
<Grid container alignItems="center" justifyContent="center" direction="row" spacing={2}> | ||
<Grid item xs={6}> | ||
<TextField | ||
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" justifyContent="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" justifyContent="center" style={{ paddingTop: '8px' }}> | ||
<CircularProgress style={{ display: formValues.loading ? 'block' : 'none' }} /> | ||
<Button variant="contained" color="primary" type="submit" style={{ display: formValues.loading ? 'none' : 'block' }}> | ||
Submit | ||
</Button> | ||
</Grid> | ||
</Grid> | ||
</form> | ||
</Container> | ||
|
||
); | ||
}; | ||
|
||
export default ContactForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import { makeStyles } from '@material-ui/core/styles'; | ||
import coverImage from '@assets/contact_bg.png'; | ||
|
||
const useStyles = makeStyles(() => ({ | ||
contactImageCover: { | ||
height: '25vh', | ||
backgroundImage: `url(${coverImage})`, | ||
backgroundPosition: 'top', | ||
backgroundRepeat: 'no-repeat', | ||
backgroundSize: 'cover', | ||
position: 'relative', | ||
|
||
}, | ||
contactImageOverlayText: { | ||
left: '50%', | ||
fontSize: '40px', | ||
fontWeight: 'bold', | ||
position: 'absolute', | ||
textAlign: 'center', | ||
top: '50%', | ||
transform: 'translate(-50%, -50%)', | ||
}, | ||
})); | ||
|
||
const ContactImage = ({ children }) => { | ||
const classes = useStyles(); | ||
|
||
return ( | ||
<div className={classes.contactImageCover}> | ||
<div className={classes.contactImageOverlayText}> | ||
<span>{children}</span> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
ContactImage.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
}; | ||
|
||
export default ContactImage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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\'t See What You Need?'} | ||
</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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Configuration settings used by the contact page | ||
const contactSettings = { | ||
edwinjue marked this conversation as resolved.
Show resolved
Hide resolved
|
||
toast: { | ||
dark: { | ||
position: 'top-right', | ||
autoClose: 5000, | ||
hideProgressBar: false, | ||
closeOnClick: true, | ||
pauseOnHover: true, | ||
draggable: true, | ||
progress: undefined, | ||
}, | ||
}, | ||
}; | ||
|
||
export default contactSettings; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?