src/client/routes/application/Application.tsx
import React, { useContext, FunctionComponent, useState, useEffect, useCallback, FC } from 'react';import styled from 'styled-components';import { useImmer } from 'use-immer';import { toast } from 'react-toastify';import { Spinner } from '../../components/Loading/Spinner';import config from '../../assets/application';import { Collapsible } from '../../components/Containers/Collapsible';import FloatingPopup from '../../components/Containers/FloatingPopup';import { ActionButtonContext } from '../../contexts/ActionButtonContext';import { Button } from '../../components/Buttons/Button';import { InputProps } from '../../components/Input/TextInput';import { useUpdateMyApplicationMutation, useMyApplicationQuery, ApplicationInput, ApplicationStatus,} from '../../generated/graphql';import { GraphQLErrorMessage } from '../../components/Text/ErrorMessage'; export interface ConfigSection { category: string; fields: ConfigField[]; title: string;} export interface ConfigField { Component: FC<InputProps>; default?: string; fieldName: string; note?: string; optional?: boolean; options?: Promise<{ data: string[] }> | string[]; other?: boolean; placeholder?: string; prompt?: string; required?: boolean; title?: string; validation?: string;} export const StyledForm = styled.form` display: flex; max-width: 100%; flex-direction: column; max-width: 100%; fieldset { margin-top: 0.4rem; } & > div:not(:first-of-type) { margin-top: 1.4rem; }`; export const StyledQuestion = styled.label` display: flex; flex-flow: column nowrap; font-size: 1rem; & > input { border-radius: 4px; background: white; }`; export const StyledQuestionPadContainer = styled.div` margin-bottom: 0.4rem;`; export const FieldPrompt = styled.h3` font-weight: lighter; font-size: 1em;`; export const FieldNote = styled.span` font-style: italic; font-weight: lighter; font-size: 1em;`; export const FieldTitle = styled.span` display: inline-block; font-style: normal; font-size: 1em; line-height: 140%;`; const disableEnter = (e: React.KeyboardEvent<HTMLFormElement>): void => { if (e.key === 'Enter') e.preventDefault();}; let autosaveTimeout: NodeJS.Timeout; /** * Finds the first element that is required (not optional) but does not have any input. * It should only be run on form submit because it is quite heavy. * @param input List of application inputs */const findRequiredUnfilled = (input: ApplicationInput[]): JSX.Element => { const requiredQuestion = config .flatMap(section => section.fields as ConfigField[]) .find( field => !field.optional && !input.find(el => el.question === field.fieldName && el.answer.trim()) ); return requiredQuestion ? ( <p> <em className="toast-emphasize">{requiredQuestion.title}</em> is required </p> ) : ( <></> );}; Function `Application` has 129 lines of code (exceeds 25 allowed). Consider refactoring.
Function `Application` has a Cognitive Complexity of 11 (exceeds 5 allowed). Consider refactoring.export const Application: FunctionComponent = (): JSX.Element => { const { update: setActionButton } = useContext(ActionButtonContext); const [openSection, setOpenSection] = useState(''); const [input, setInput] = useImmer<{ answer: string; question: string }[]>([]); const [loaded, setLoaded] = useState(false); const [updateApplication] = useUpdateMyApplicationMutation(); const { data, error, loading } = useMyApplicationQuery(); // Only update changed QuestionIDs const createOnChangeHandler = (fieldName: string): ((value: string) => void) => value => { void setInput(draft => { const element = draft.find(el => el.question === fieldName); if (!element) { draft.push({ answer: value, question: fieldName }); } else { element.answer = value; } }); }; const valueHandler = (fieldName: string): string => { const element = input.find(el => el.question === fieldName); return element ? element.answer : ''; }; useEffect((): (() => void) => { if (setActionButton) setActionButton( <Button async large onClick={async () => { const firstRequiredUnfilledToast = findRequiredUnfilled(input); toast.dismiss(); if (firstRequiredUnfilledToast !== <></>) toast.error(firstRequiredUnfilledToast, { position: 'bottom-right', }); else return updateApplication({ variables: { input: { fields: input, submit: true } }, }).then(() => { toast.dismiss(); return toast.success('Application submitted successfully!', { position: 'bottom-right', }); }); return Promise.resolve(); }}> Submit </Button> ); return () => { if (setActionButton) setActionButton(undefined); }; }, [input, setActionButton, updateApplication]); useEffect((): void => { if (!loaded && data && data.me && data.me.__typename === 'Hacker') { const { application } = data.me; setInput(draft => { draft.length = 0; // Clear the array // Omit the `__typename` field. application.forEach(({ question, answer }) => draft.push({ answer: answer || '', question }) ); }); setOpenSection(config[0].title); setLoaded(true); } }, [data, loaded, setLoaded, setInput]); useEffect(() => { // Auto-save application input after five seconds of inactivity. autosaveTimeout = setTimeout( () => input.length && // Do not auto-save after a hacker has been accepted to workaround:TODO found // TODO: Prevent hackers from removing fields and letting auto-save wipe // them from the DB, then remove this workaround. !( data && data.me && data.me.__typename === 'Hacker' && [ ApplicationStatus.Accepted, ApplicationStatus.Confirmed, ApplicationStatus.Rejected, ].includes(data.me.status) ) && updateApplication({ variables: { input: { fields: input, submit: false } } }), 5000 ); // Cleanup return () => clearTimeout(autosaveTimeout); }, [data, input, updateApplication]); const toggleOpen = useCallback( (e: React.MouseEvent<HTMLButtonElement>): void => { const { id } = e.currentTarget; setOpenSection(openSection === id ? '' : id); e.preventDefault(); }, [openSection] ); // if error getting application input if (error) { console.log(error); return <GraphQLErrorMessage text={JSON.stringify(error)} />; } if (loading) return <Spinner />; return ( <FloatingPopup // height="100%" width="100%" backgroundOpacity="1" justifyContent="flex-start" alignItems="flex-start" padding="1.5rem"> <StyledForm onKeyPress={disableEnter}> {config.map(({ fields, title = '' }: ConfigSection) => ( <Collapsible onClick={toggleOpen} open={openSection === title} title={title} key={title}> {fields.map(field => ( <StyledQuestion key={field.fieldName} htmlFor={field.fieldName}> {field.title ? ( <StyledQuestionPadContainer> {field.title} {!field.optional ? `*` : null} {field.note ? <FieldNote>{` - ${field.note}`}</FieldNote> : null} </StyledQuestionPadContainer> ) : null} {field.prompt ? <FieldPrompt>{field.prompt}</FieldPrompt> : null} <field.Component setState={createOnChangeHandler(field.fieldName)} value={valueHandler(field.fieldName)} {...field} id={field.fieldName} /> </StyledQuestion> ))} </Collapsible> ))} </StyledForm> </FloatingPopup> );}; export default Application;