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

Submit form (#551)

- Fetch a token for form submission
- Submit the form
- Show the spinner while the form is being submitted
parent 9538bd08
Pipeline #185993 passed with stages
in 4 minutes and 23 seconds
...@@ -89,9 +89,12 @@ $columnGapWidth: 12px; ...@@ -89,9 +89,12 @@ $columnGapWidth: 12px;
} }
} }
.submitButton { .submitButtonWrapper {
grid-column: submit-right; grid-column: submit-right;
justify-self: end; justify-self: end;
}
.submitButton {
height: 30px; height: 30px;
width: 90px; width: 90px;
} }
......
...@@ -25,14 +25,17 @@ import classNames from 'classnames'; ...@@ -25,14 +25,17 @@ import classNames from 'classnames';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { submitForm } from '../submitForm'; import { submitForm } from '../submitForm';
import noEarlierThan from 'src/shared/utils/noEarlierThan';
import SubmissionSuccess from '../submission-success/SubmissionSuccess'; import SubmissionSuccess from '../submission-success/SubmissionSuccess';
import ShadedInput from 'src/shared/components/input/ShadedInput'; import ShadedInput from 'src/shared/components/input/ShadedInput';
import ShadedTextarea from 'src/shared/components/textarea/ShadedTextarea'; import ShadedTextarea from 'src/shared/components/textarea/ShadedTextarea';
import Upload from 'src/shared/components/upload/Upload'; import Upload from 'src/shared/components/upload/Upload';
import UploadedFile from 'src/shared/components/uploaded-file/UploadedFile'; import UploadedFile from 'src/shared/components/uploaded-file/UploadedFile';
import { PrimaryButton } from 'src/shared/components/button/Button';
import SubmitSlider from '../submit-slider/SubmitSlider'; import SubmitSlider from '../submit-slider/SubmitSlider';
import { ControlledLoadingButton } from 'src/shared/components/loading-button';
import { LoadingState } from 'src/shared/types/loading-state';
import commonStyles from '../ContactUsForm.scss'; import commonStyles from '../ContactUsForm.scss';
...@@ -111,12 +114,16 @@ const reducer = (state: State, action: Action): State => { ...@@ -111,12 +114,16 @@ const reducer = (state: State, action: Action): State => {
} }
}; };
const FORM_NAME = 'contact-us-general';
const ContactUsInitialForm = () => { const ContactUsInitialForm = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const [isChallengeCompleted, setIsChallengeCompleted] = useState(false); const [isChallengeCompleted, setIsChallengeCompleted] = useState(false);
const [emailFieldValid, setEmailFieldValid] = useState(true); const [emailFieldValid, setEmailFieldValid] = useState(true);
const [emailFieldFocussed, setEmailFieldFocussed] = useState(false); const [emailFieldFocussed, setEmailFieldFocussed] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false); const [submissionState, setSubmissionState] = useState<LoadingState>(
LoadingState.NOT_REQUESTED
);
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const emailFieldRef = useRef<HTMLInputElement | null>(null); const emailFieldRef = useRef<HTMLInputElement | null>(null);
...@@ -174,13 +181,30 @@ const ContactUsInitialForm = () => { ...@@ -174,13 +181,30 @@ const ContactUsInitialForm = () => {
const handleSubmit = useCallback((e: React.SyntheticEvent) => { const handleSubmit = useCallback((e: React.SyntheticEvent) => {
e.preventDefault(); e.preventDefault();
stateRef.current && submitForm(stateRef.current); if (!stateRef.current) {
setIsSubmitted(true); return; // shouldn't happen, but makes Typescript happy
}
setSubmissionState(LoadingState.LOADING);
const submitPromise = submitForm({
...stateRef.current,
form_type: FORM_NAME
});
noEarlierThan(submitPromise, 1000)
.then(() => {
setSubmissionState(LoadingState.SUCCESS);
})
.catch(() => {
setSubmissionState(LoadingState.ERROR);
setTimeout(() => setSubmissionState(LoadingState.NOT_REQUESTED), 2000);
});
}, []); }, []);
const isFormValid = validate(state) && emailFieldValid; const isFormValid = validate(state) && emailFieldValid;
if (isSubmitted) { if (submissionState === LoadingState.SUCCESS) {
return <SubmissionSuccess />; return <SubmissionSuccess />;
} }
...@@ -271,14 +295,17 @@ const ContactUsInitialForm = () => { ...@@ -271,14 +295,17 @@ const ContactUsInitialForm = () => {
)} )}
{isChallengeCompleted ? ( {isChallengeCompleted ? (
<PrimaryButton <ControlledLoadingButton
type="submit" status={submissionState}
className={commonStyles.submitButton} classNames={{
wrapper: commonStyles.submitButtonWrapper,
button: commonStyles.submitButton
}}
isDisabled={!isFormValid} isDisabled={!isFormValid}
onClick={noop} onClick={noop}
> >
Send Send
</PrimaryButton> </ControlledLoadingButton>
) : ( ) : (
<> <>
<span <span
......
...@@ -18,9 +18,34 @@ type FormFieldType = string | File | File[] | null; ...@@ -18,9 +18,34 @@ type FormFieldType = string | File | File[] | null;
type Form = Record<string, FormFieldType>; type Form = Record<string, FormFieldType>;
export const submitForm = (form: Form) => { type TokenSuccessResponse = {
token: string;
};
export const submitForm = async (form: Form) => {
const formData = buildFormData(form); const formData = buildFormData(form);
console.log('formData', formData); // eslint-disable-line no-console const url = '/api/support/ticket';
const token = await getToken();
return fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`
},
body: formData
}).then(async (response) => {
let responseMessage;
try {
responseMessage = await response.json();
} catch {
throw new Error();
}
if (!response.ok) {
throw new Error(responseMessage);
} else {
return responseMessage;
}
});
}; };
const buildFormData = (form: Form) => { const buildFormData = (form: Form) => {
...@@ -49,3 +74,11 @@ const addToFormData = ( ...@@ -49,3 +74,11 @@ const addToFormData = (
formData.append(fieldName, value); formData.append(fieldName, value);
} }
}; };
const getToken = async () => {
const url = '/api/support/token';
const { token } = (await fetch(url).then((response) =>
response.json()
)) as TokenSuccessResponse;
return token;
};
...@@ -31,20 +31,28 @@ type Props = { ...@@ -31,20 +31,28 @@ type Props = {
onClick: () => unknown; onClick: () => unknown;
status: LoadingState; status: LoadingState;
isDisabled?: boolean; isDisabled?: boolean;
className?: string; classNames?: {
wrapper?: string;
button?: string;
};
children: ReactNode; children: ReactNode;
}; };
const ControlledLoadingButton = (props: Props) => { const ControlledLoadingButton = (props: Props) => {
const { status: loadingState, className, ...otherProps } = props; const {
status: loadingState,
classNames: { wrapper: wrapperClassName, button: buttonClassName } = {},
...otherProps
} = props;
const wrapperClass = classNames(styles.buttonWrapper, wrapperClassName);
const buttonClass = const buttonClass =
loadingState !== LoadingState.NOT_REQUESTED loadingState !== LoadingState.NOT_REQUESTED
? classNames(className, styles.invisible) ? classNames(buttonClassName, styles.invisible)
: className; : buttonClassName;
return ( return (
<div className={styles.buttonWrapper}> <div className={wrapperClass}>
{loadingState === LoadingState.LOADING && <Loading />} {loadingState === LoadingState.LOADING && <Loading />}
{loadingState === LoadingState.SUCCESS && <Success />} {loadingState === LoadingState.SUCCESS && <Success />}
{loadingState === LoadingState.ERROR && <ErrorIndicator />} {loadingState === LoadingState.ERROR && <ErrorIndicator />}
......
...@@ -27,7 +27,10 @@ type LoadingButtonProps = { ...@@ -27,7 +27,10 @@ type LoadingButtonProps = {
onSuccess?: (x?: unknown) => void; onSuccess?: (x?: unknown) => void;
onError?: (x?: unknown) => void; onError?: (x?: unknown) => void;
isDisabled?: boolean; isDisabled?: boolean;
className?: string; classNames?: {
wrapper?: string;
button?: string;
};
children: ReactNode; children: ReactNode;
}; };
...@@ -98,7 +101,7 @@ const LoadingButton = (props: LoadingButtonProps) => { ...@@ -98,7 +101,7 @@ const LoadingButton = (props: LoadingButtonProps) => {
<ControlledLoadingButton <ControlledLoadingButton
status={loadingState} status={loadingState}
onClick={onClick} onClick={onClick}
className={props.className} classNames={props.classNames}
isDisabled={props.isDisabled} isDisabled={props.isDisabled}
> >
{props.children} {props.children}
......
/**
* 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 { from, timer, combineLatest, firstValueFrom } from 'rxjs';
import { take } from 'rxjs/operators';
const noEarlierThan = async <P extends Promise<any>>(
promise: P,
minimumTime: number
) => {
const source$ = combineLatest([
timer(minimumTime).pipe(take(1)),
from(promise)
]);
const [, result] = await firstValueFrom(source$);
return result;
};
export default noEarlierThan;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment