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;
}
}
.submitButton {
.submitButtonWrapper {
grid-column: submit-right;
justify-self: end;
}
.submitButton {
height: 30px;
width: 90px;
}
......
......@@ -25,14 +25,17 @@ import classNames from 'classnames';
import noop from 'lodash/noop';
import { submitForm } from '../submitForm';
import noEarlierThan from 'src/shared/utils/noEarlierThan';
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 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';
......@@ -111,12 +114,16 @@ const reducer = (state: State, action: Action): State => {
}
};
const FORM_NAME = 'contact-us-general';
const ContactUsInitialForm = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const [isChallengeCompleted, setIsChallengeCompleted] = useState(false);
const [emailFieldValid, setEmailFieldValid] = useState(true);
const [emailFieldFocussed, setEmailFieldFocussed] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [submissionState, setSubmissionState] = useState<LoadingState>(
LoadingState.NOT_REQUESTED
);
const formRef = useRef<HTMLFormElement>(null);
const emailFieldRef = useRef<HTMLInputElement | null>(null);
......@@ -174,13 +181,30 @@ const ContactUsInitialForm = () => {
const handleSubmit = useCallback((e: React.SyntheticEvent) => {
e.preventDefault();
stateRef.current && submitForm(stateRef.current);
setIsSubmitted(true);
if (!stateRef.current) {
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;
if (isSubmitted) {
if (submissionState === LoadingState.SUCCESS) {
return <SubmissionSuccess />;
}
......@@ -271,14 +295,17 @@ const ContactUsInitialForm = () => {
)}
{isChallengeCompleted ? (
<PrimaryButton
type="submit"
className={commonStyles.submitButton}
<ControlledLoadingButton
status={submissionState}
classNames={{
wrapper: commonStyles.submitButtonWrapper,
button: commonStyles.submitButton
}}
isDisabled={!isFormValid}
onClick={noop}
>
Send
</PrimaryButton>
</ControlledLoadingButton>
) : (
<>
<span
......
......@@ -18,9 +18,34 @@ type FormFieldType = string | File | File[] | null;
type Form = Record<string, FormFieldType>;
export const submitForm = (form: Form) => {
type TokenSuccessResponse = {
token: string;
};
export const submitForm = async (form: 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) => {
......@@ -49,3 +74,11 @@ const addToFormData = (
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 = {
onClick: () => unknown;
status: LoadingState;
isDisabled?: boolean;
className?: string;
classNames?: {
wrapper?: string;
button?: string;
};
children: ReactNode;
};
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 =
loadingState !== LoadingState.NOT_REQUESTED
? classNames(className, styles.invisible)
: className;
? classNames(buttonClassName, styles.invisible)
: buttonClassName;
return (
<div className={styles.buttonWrapper}>
<div className={wrapperClass}>
{loadingState === LoadingState.LOADING && <Loading />}
{loadingState === LoadingState.SUCCESS && <Success />}
{loadingState === LoadingState.ERROR && <ErrorIndicator />}
......
......@@ -27,7 +27,10 @@ type LoadingButtonProps = {
onSuccess?: (x?: unknown) => void;
onError?: (x?: unknown) => void;
isDisabled?: boolean;
className?: string;
classNames?: {
wrapper?: string;
button?: string;
};
children: ReactNode;
};
......@@ -98,7 +101,7 @@ const LoadingButton = (props: LoadingButtonProps) => {
<ControlledLoadingButton
status={loadingState}
onClick={onClick}
className={props.className}
classNames={props.classNames}
isDisabled={props.isDisabled}
>
{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