Unverified Commit a1314d42 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Add contact us form (#541)

parent 257afe8e
Pipeline #181318 passed with stages
in 4 minutes and 32 seconds
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
"dotenv": "10.0.0", "dotenv": "10.0.0",
"ensembl-genome-browser": "https://raw.githubusercontent.com/Ensembl/ensembl-genome-browser-assets/master/assets-80f51620ed443c640cdfd6b5aebd505b.tar.gz", "ensembl-genome-browser": "https://raw.githubusercontent.com/Ensembl/ensembl-genome-browser-assets/master/assets-80f51620ed443c640cdfd6b5aebd505b.tar.gz",
"express": "4.17.1", "express": "4.17.1",
"filesize": "7.0.0",
"graphql": "15.5.1", "graphql": "15.5.1",
"http-proxy-middleware": "2.0.1", "http-proxy-middleware": "2.0.1",
"lodash": "4.17.21", "lodash": "4.17.21",
...@@ -20593,10 +20594,9 @@ ...@@ -20593,10 +20594,9 @@
} }
}, },
"node_modules/filesize": { "node_modules/filesize": {
"version": "6.1.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-7.0.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "integrity": "sha512-Wsstw+O1lZ9gVmOI1thyeQvODsaoId2qw14lCqIzUhoHKXX7T2hVpB7BR6SvgodMBgWccrx/y2eyV8L7tDmY6A==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
...@@ -34719,6 +34719,15 @@ ...@@ -34719,6 +34719,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/react-dev-utils/node_modules/filesize": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/react-dev-utils/node_modules/fill-range": { "node_modules/react-dev-utils/node_modules/fill-range": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
...@@ -58574,10 +58583,9 @@ ...@@ -58574,10 +58583,9 @@
} }
}, },
"filesize": { "filesize": {
"version": "6.1.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-7.0.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", "integrity": "sha512-Wsstw+O1lZ9gVmOI1thyeQvODsaoId2qw14lCqIzUhoHKXX7T2hVpB7BR6SvgodMBgWccrx/y2eyV8L7tDmY6A=="
"dev": true
}, },
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
...@@ -69264,6 +69272,12 @@ ...@@ -69264,6 +69272,12 @@
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true "dev": true
}, },
"filesize": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
"integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==",
"dev": true
},
"fill-range": { "fill-range": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
"dotenv": "10.0.0", "dotenv": "10.0.0",
"ensembl-genome-browser": "https://raw.githubusercontent.com/Ensembl/ensembl-genome-browser-assets/master/assets-80f51620ed443c640cdfd6b5aebd505b.tar.gz", "ensembl-genome-browser": "https://raw.githubusercontent.com/Ensembl/ensembl-genome-browser-assets/master/assets-80f51620ed443c640cdfd6b5aebd505b.tar.gz",
"express": "4.17.1", "express": "4.17.1",
"filesize": "7.0.0",
"graphql": "15.5.1", "graphql": "15.5.1",
"http-proxy-middleware": "2.0.1", "http-proxy-middleware": "2.0.1",
"lodash": "4.17.21", "lodash": "4.17.21",
......
...@@ -14,16 +14,17 @@ ...@@ -14,16 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { ReactNode } from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './Button.scss'; import styles from './Button.scss';
type Props = { type Props = Omit<
React.HTMLProps<HTMLButtonElement>,
'disabled' | 'onClick'
> & {
onClick: () => void; onClick: () => void;
isDisabled?: boolean; isDisabled?: boolean;
className?: string;
children: ReactNode;
}; };
export const PrimaryButton = (props: Props) => { export const PrimaryButton = (props: Props) => {
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ContactUsInitialForm } from './contact-us-form';
import { Invitation, Header } from './preform-header/PreformHeader';
import { SecondaryButton } from 'src/shared/components/button/Button'; import { SecondaryButton } from 'src/shared/components/button/Button';
import ExternalLink from 'src/shared/components/external-link/ExternalLink'; import ExternalLink from 'src/shared/components/external-link/ExternalLink';
...@@ -29,15 +31,22 @@ const ContactUs = () => { ...@@ -29,15 +31,22 @@ const ContactUs = () => {
const [shouldShowForm, setShouldShowForm] = useState(false); const [shouldShowForm, setShouldShowForm] = useState(false);
if (shouldShowForm) { if (shouldShowForm) {
return <div>Will display the form</div>; return (
<div>
<Invitation />
<Header
title="Send us a message"
onClick={() => setShouldShowForm(false)}
/>
<ContactUsInitialForm />
</div>
);
} }
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<section> <section>
<p> <Invitation />
Please contact us if you have a problem with the website or need help
</p>
<SecondaryButton onClick={() => setShouldShowForm(!shouldShowForm)}> <SecondaryButton onClick={() => setShouldShowForm(!shouldShowForm)}>
Contact us Contact us
</SecondaryButton> </SecondaryButton>
......
@import 'src/styles/common';
$maxFormWidth: 520px;
$leftColumnWidth: 135px;
$columnGapWidth: 12px;
.container {
background: $light-grey;
padding: 12px 0 52px 30px;
min-height: 570px;
}
.grid {
display: grid;
grid-template-columns: [left] $leftColumnWidth [right] 1fr;
column-gap: 12px;
row-gap: 6px;
width: 100%;
max-width: $maxFormWidth;
}
.advisory {
font-size: 12px;
font-weight: $light;
grid-column: right;
span {
display: block;
}
}
.label {
justify-self: right;
margin-top: 6px;
font-size: 12px;
font-weight: $light;
}
.textarea {
height: 150px;
}
.upload {
grid-column: right;
margin-top: 18px;
max-width: calc(#{$maxFormWidth} - #{$leftColumnWidth} - #{$columnGapWidth});
}
.uploadedFile {
margin-bottom: 12px;
}
.submit {
display: flex;
align-items: center;
gap: 1rem;
grid-column: right;
justify-self: right;
margin-top: 30px;
}
.errorText {
color: $red;
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The intention is to eventually have multiple forms
* designed to address different problems. This index file
* will list the multiple forms.
*/
export { default as ContactUsInitialForm } from './initial/ContactUsInitialForm';
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState, useReducer, useCallback, useRef } from 'react';
import noop from 'lodash/noop';
import { submitForm } from '../submitForm';
import SubmissionSuccess from '../submission-success/SubmissionSuccess';
import ShadedInput from 'src/shared/components/input/ShadedInput';
import ShadedTextarea from 'src/shared/components/textarea/ShadedTextarea';
import Upload from 'src/shared/components/upload/Upload';
import UploadedFile from 'src/shared/components/uploaded-file/UploadedFile';
import { PrimaryButton } from 'src/shared/components/button/Button';
import commonStyles from '../ContactUsForm.scss';
type State = {
name: string;
email: string;
subject: string;
message: string;
files: File[];
};
const initialState: State = {
name: '',
email: '',
subject: '',
message: '',
files: []
};
type UpdateNameAction = {
type: 'update-name';
payload: string;
};
type UpdateEmailAction = {
type: 'update-email';
payload: string;
};
type UpdateSubjectAction = {
type: 'update-subject';
payload: string;
};
type UpdateMessageAction = {
type: 'update-message';
payload: string;
};
type AddFileAction = {
type: 'add-file';
payload: File;
};
type RemoveFileAction = {
type: 'remove-file';
payload: number; // index of the file in the array of files
};
type Action =
| UpdateNameAction
| UpdateEmailAction
| UpdateSubjectAction
| UpdateMessageAction
| AddFileAction
| RemoveFileAction;
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'update-name':
return { ...state, name: action.payload };
case 'update-email':
return { ...state, email: action.payload };
case 'update-subject':
return { ...state, subject: action.payload };
case 'update-message':
return { ...state, message: action.payload };
case 'add-file':
return { ...state, files: [...state.files, action.payload] };
case 'remove-file':
const newFiles = [...state.files];
newFiles.splice(action.payload, 1);
return { ...state, files: newFiles };
default:
return state;
}
};
const ContactUsInitialForm = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const [isSubmitted, setIsSubmitted] = useState(false);
const stateRef = useRef<typeof state>();
stateRef.current = state;
const onNameChange = useCallback((value: string) => {
dispatch({ type: 'update-name', payload: value });
}, []);
const onEmailChange = useCallback((value: string) => {
dispatch({ type: 'update-email', payload: value });
}, []);
const onSubjectChange = useCallback((value: string) => {
dispatch({ type: 'update-subject', payload: value });
}, []);
const onMessageChange = useCallback((value: string) => {
dispatch({ type: 'update-message', payload: value });
}, []);
const onFileChange = useCallback((file: File) => {
dispatch({ type: 'add-file', payload: file });
}, []);
const deleteFile = (index: number) => {
dispatch({ type: 'remove-file', payload: index });
};
const handleSubmit = useCallback((e: React.SyntheticEvent) => {
e.preventDefault();
stateRef.current && submitForm(stateRef.current);
setIsSubmitted(true);
}, []);
const isFormValid = validate(state);
if (isSubmitted) {
return <SubmissionSuccess />;
}
return (
<div className={commonStyles.container}>
<div className={commonStyles.grid}>
<p className={commonStyles.advisory}>
<span>All fields are required unless marked optional</span>
<span>Second line</span>
<span>Third line</span>
</p>
</div>
<form
className={commonStyles.grid}
autoComplete="off"
onSubmit={handleSubmit}
>
<label htmlFor="name" className={commonStyles.label}>
Your name
</label>
<ShadedInput id="name" value={state.name} onChange={onNameChange} />
<label htmlFor="email" className={commonStyles.label}>
Your email
</label>
<ShadedInput id="email" value={state.email} onChange={onEmailChange} />
<label htmlFor="subject" className={commonStyles.label}>
Subject
</label>
<ShadedInput
id="subject"
value={state.subject}
onChange={onSubjectChange}
/>
<label htmlFor="message" className={commonStyles.label}>
Message
</label>
<ShadedTextarea
id="message"
value={state.message}
onChange={onMessageChange}
className={commonStyles.textarea}
/>
<div className={commonStyles.upload}>
{state.files.map((file, index) => (
<UploadedFile
key={index}
file={file}
onDelete={() => deleteFile(index)}
classNames={{ wrapper: commonStyles.uploadedFile }}
/>
))}
<Upload
label="Click or drag a file here to upload"
callbackWithFiles={true}
allowMultiple={false}
onChange={onFileChange}
/>
</div>
<div className={commonStyles.submit}>
{exceedsAttachmentsSizeLimit(state) && (
<span className={commonStyles.errorText}>
Attachment(s) exceed 10 MB
</span>
)}
<PrimaryButton type="submit" isDisabled={!isFormValid} onClick={noop}>
Submit
</PrimaryButton>
</div>
</form>
</div>
);
};
const validate = (formState: State) => {
return (
areMandatoryFieldsFilled(formState) &&
!exceedsAttachmentsSizeLimit(formState)
);
};
const areMandatoryFieldsFilled = (formState: State) => {
return (['name', 'email', 'subject', 'message'] as const).every(
(field) => formState[field]
);
};
const getTotalFilesSize = (formState: State) => {
return formState.files.reduce((sum, file) => sum + file.size, 0);
};
const exceedsAttachmentsSizeLimit = (formState: State) => {
const attachmentsSizeLimit = 10e6; // 10 MB according to SI units (where 1 megabyte is exactly 1 million bytes)
const totalFileSize = getTotalFilesSize(formState);
return totalFileSize > attachmentsSizeLimit;
};
export default ContactUsInitialForm;
.submissionSuccess {
p {
margin-bottom: 1rem;
}
.thankyou {
margin: 2rem 0 1.5rem;
}
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { PrimaryButton } from 'src/shared/components/button/Button';
import styles from './SubmissionSuccess.scss';
const SubmissionSuccess = () => {
const onButtonClick = () => {}; // eslint-disable-line
return (
<div className={styles.submissionSuccess}>
<p>Your message has been sent to our HelpDesk</p>
<p>
You should receive an auto-reply with a ticket number within 24 hours
<br />
If you do not get this, please try again, checking your email address
</p>
<p className={styles.thankyou}>Thank you</p>