From 5cddaa34e8dc272927367e549dcabc1c72d16afe Mon Sep 17 00:00:00 2001 From: aleinin Date: Tue, 5 Dec 2023 20:06:18 -0600 Subject: [PATCH] validation fixes --- .../src/components/controls/Search.tsx | 11 ++- .../src/components/submission/BestOldGame.tsx | 19 ++-- .../src/components/submission/GOTY.tsx | 13 ++- .../components/submission/MostAnticipated.tsx | 19 ++-- .../submission/MostDisappointing.tsx | 21 ++-- .../SubmissionForm/SubmissionForm.tsx | 36 ++++--- .../components/submission/SubmissionHub.tsx | 67 ------------- .../submission/SubmissionRouter.tsx | 4 +- .../submission/useSubmissionForm.ts | 97 +++++++++++++++++++ .../src/util/useUnsavedChangesWarning.ts | 6 +- 10 files changed, 170 insertions(+), 123 deletions(-) delete mode 100644 goty-client/src/components/submission/SubmissionHub.tsx create mode 100644 goty-client/src/components/submission/useSubmissionForm.ts diff --git a/goty-client/src/components/controls/Search.tsx b/goty-client/src/components/controls/Search.tsx index 6c416e5..63a864d 100644 --- a/goty-client/src/components/controls/Search.tsx +++ b/goty-client/src/components/controls/Search.tsx @@ -1,7 +1,11 @@ -import { useState } from 'react' +import { useContext, useState } from 'react' import { GameService } from '../../api/gameService' import { Game } from '../../models/game' import { AutoComplete } from './AutoComplete/AutoComplete' +import { + InputStateKeyContext, + SubmissionInputContext, +} from '../submission/useSubmissionForm' export interface SearchProps { placeholder: string @@ -10,7 +14,10 @@ export interface SearchProps { } export const Search = (props: SearchProps) => { - const [input, setInput] = useState('') + const key = useContext(InputStateKeyContext) + const { + [key]: [input, setInput], + } = useContext(SubmissionInputContext) const [suggestions, setSuggestions] = useState([]) const handleSearch = (searchText: string) => GameService.searchGames({ diff --git a/goty-client/src/components/submission/BestOldGame.tsx b/goty-client/src/components/submission/BestOldGame.tsx index 95bd40e..ea07ff8 100644 --- a/goty-client/src/components/submission/BestOldGame.tsx +++ b/goty-client/src/components/submission/BestOldGame.tsx @@ -1,6 +1,7 @@ import { Game } from '../../models/game' import { useProperties } from '../../api/useProperties' import { SingleGame } from '../controls/SingleGame' +import { InputStateKeyContext } from './useSubmissionForm' export interface BestOldGameProps { readonly: boolean @@ -20,13 +21,15 @@ export const BestOldGame = ({ handleSetBestOldGame && handleSetBestOldGame(bestOldGame) } return ( - + + + ) } diff --git a/goty-client/src/components/submission/GOTY.tsx b/goty-client/src/components/submission/GOTY.tsx index d56064d..43e966e 100644 --- a/goty-client/src/components/submission/GOTY.tsx +++ b/goty-client/src/components/submission/GOTY.tsx @@ -5,6 +5,7 @@ import { Rules } from './Rules' import { useProperties } from '../../api/useProperties' import { Search } from '../controls/Search' import { OrderedList } from '../controls/OrderedList' +import { InputStateKeyContext } from './useSubmissionForm' export enum MoveDirection { IncreaseRank, @@ -80,11 +81,13 @@ export const GOTY = ({ games, handleSetGames, readonly }: GOTYProps) => { {!readonly && getTieBreaker(properties.tiePoints)} {readonly || games.length === properties.tiePoints.length ? null : ( - + + + )} + + + ) } diff --git a/goty-client/src/components/submission/MostDisappointing.tsx b/goty-client/src/components/submission/MostDisappointing.tsx index 6469d96..3477e9e 100644 --- a/goty-client/src/components/submission/MostDisappointing.tsx +++ b/goty-client/src/components/submission/MostDisappointing.tsx @@ -1,5 +1,6 @@ import { Game } from '../../models/game' import { SingleGame } from '../controls/SingleGame' +import { InputStateKeyContext } from './useSubmissionForm' export interface MostDisappointingProps { readonly: boolean @@ -18,14 +19,16 @@ export const MostDisappointing = ({ handleSetMostDisappointing && handleSetMostDisappointing(mostDisappointing) } return ( - + + + ) } diff --git a/goty-client/src/components/submission/SubmissionForm/SubmissionForm.tsx b/goty-client/src/components/submission/SubmissionForm/SubmissionForm.tsx index 382d854..32c5ad8 100644 --- a/goty-client/src/components/submission/SubmissionForm/SubmissionForm.tsx +++ b/goty-client/src/components/submission/SubmissionForm/SubmissionForm.tsx @@ -9,63 +9,61 @@ import { useProperties } from '../../../api/useProperties' import { useDocumentTitle } from '../../../util/useDocumentTitle' import { Game } from '../../../models/game' import { Name } from '../Name' -import { Submission } from '../../../models/submission' import { MostDisappointing } from '../MostDisappointing' +import { SubmissionInputContext, useSubmissionForm } from '../useSubmissionForm' interface SubmissionFormProps { - submission: Submission - handleSetSubmission: (submission: Submission) => void - handleSubmitSubmission: () => void - isValid: boolean + handleDone: () => void + handleError: (error: any) => void } export const SubmissionForm = ({ - submission, - handleSetSubmission, - handleSubmitSubmission, - isValid, + handleDone, + handleError, }: SubmissionFormProps) => { const { properties } = useProperties() + const { submission, setSubmission, isValid, isDirty, inputs, handleSubmit } = + useSubmissionForm(handleDone, handleError) const handleSetName = (name: string) => { - handleSetSubmission({ + setSubmission({ ...submission, name, }) } const handleSetBestOldGame = (bestOldGame: Game | null) => { - handleSetSubmission({ + setSubmission({ ...submission, bestOldGame, }) } const handleSetMostAnticipated = (mostAnticipated: Game | null) => { - handleSetSubmission({ + setSubmission({ ...submission, mostAnticipated, }) } const handleSetMostDisappointing = (mostDisappointing: Game | null) => { - handleSetSubmission({ + setSubmission({ ...submission, mostDisappointing, }) } const handleSetEnteredGiveaway = (enteredGiveaway: boolean) => { - handleSetSubmission({ + setSubmission({ ...submission, enteredGiveaway, }) } const handleSetGames = (gamesOfTheYear: Game[]) => { - handleSetSubmission({ + setSubmission({ ...submission, gamesOfTheYear, }) } useDocumentTitle('GOTY - Submission') return ( - <> + * Required @@ -104,11 +102,11 @@ export const SubmissionForm = ({ ) : null} - + ) } diff --git a/goty-client/src/components/submission/SubmissionHub.tsx b/goty-client/src/components/submission/SubmissionHub.tsx deleted file mode 100644 index 7730ad7..0000000 --- a/goty-client/src/components/submission/SubmissionHub.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { SubmissionForm } from './SubmissionForm/SubmissionForm' -import { useEffect, useState } from 'react' -import { SubmissionService } from '../../api/submissionService' -import { isEqual, Submission } from '../../models/submission' -import { useProperties } from '../../api/useProperties' -import { useSubmission } from '../../api/useSubmission' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useSubmissionIds } from '../../api/useSubmissionIds' -import { useUnsavedChangesWarning } from '../../util/useUnsavedChangesWarning' - -const submissionIsValid = ( - submission: Submission, - initialForm: Submission, - hasGiveaway: boolean, -) => - !isEqual(submission, initialForm) && - submission.name?.length > 0 && - submission.gamesOfTheYear?.length > 0 && - (!hasGiveaway || submission.enteredGiveaway != null) - -interface SubmissionHubProps { - handleDone: () => void - handleError: (error: any) => void -} -export const SubmissionHub = ({ - handleDone, - handleError, -}: SubmissionHubProps) => { - const queryClient = useQueryClient() - const { id } = useSubmissionIds() - const { properties } = useProperties() - const { data: initialSubmission } = useSubmission(id) - const [submission, setSubmission] = useState(initialSubmission) - const upsertSubmissionMutation = useMutation({ - mutationFn: (newSubmission: Submission) => - id !== '' - ? SubmissionService.updateSubmission(newSubmission) - : SubmissionService.createSubmission(newSubmission), - onSuccess: (newSubmission) => { - queryClient.setQueryData(['submission', id], newSubmission) - handleDone() - }, - onError: (error) => { - handleError(error) - }, - }) - const isValid = submissionIsValid( - submission, - initialSubmission, - properties.hasGiveaway, - ) - useUnsavedChangesWarning(isValid) - useEffect(() => { - setSubmission(initialSubmission) - }, [initialSubmission]) - const handleSubmit = () => { - upsertSubmissionMutation.mutate(submission) - } - return ( - - ) -} diff --git a/goty-client/src/components/submission/SubmissionRouter.tsx b/goty-client/src/components/submission/SubmissionRouter.tsx index 584e937..7fbdb96 100644 --- a/goty-client/src/components/submission/SubmissionRouter.tsx +++ b/goty-client/src/components/submission/SubmissionRouter.tsx @@ -3,7 +3,7 @@ import { End } from './End/End' import { isGotyConcluded } from '../../util/isGotyConcluded' import { useProperties } from '../../api/useProperties' import { Concluded } from './Concluded' -import { SubmissionHub } from './SubmissionHub' +import { SubmissionForm } from './SubmissionForm/SubmissionForm' export const SubmissionRouter = () => { const [isDone, setIsDone] = useState(false) @@ -21,7 +21,7 @@ export const SubmissionRouter = () => { return isDone ? ( setIsDone(false)} /> ) : ( - setIsDone(true)} handleError={handleError} /> diff --git a/goty-client/src/components/submission/useSubmissionForm.ts b/goty-client/src/components/submission/useSubmissionForm.ts new file mode 100644 index 0000000..3f863ff --- /dev/null +++ b/goty-client/src/components/submission/useSubmissionForm.ts @@ -0,0 +1,97 @@ +import { useProperties } from '../../api/useProperties' +import { isEqual, Submission } from '../../models/submission' +import { createContext, useCallback, useEffect, useState } from 'react' +import { useUnsavedChangesWarning } from '../../util/useUnsavedChangesWarning' +import { useSubmission } from '../../api/useSubmission' +import { useSubmissionIds } from '../../api/useSubmissionIds' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { SubmissionService } from '../../api/submissionService' + +type InputState = [string, (value: string) => void] +const initialInputState: InputState = ['', () => {}] +export interface SubmissionInputsState { + goty: InputState + mostDisappointing: InputState + mostAnticipated: InputState + bestOldGame: InputState +} +export const SubmissionInputContext = createContext({ + goty: initialInputState, + mostDisappointing: initialInputState, + mostAnticipated: initialInputState, + bestOldGame: initialInputState, +}) +export const InputStateKeyContext = + createContext('goty') +export interface SubmissionInputsState { + goty: InputState + mostDisappointing: InputState + mostAnticipated: InputState + bestOldGame: InputState +} + +const submissionIsValid = ( + submission: Submission, + hasGiveaway: boolean, + inputs: SubmissionInputsState, +) => + submission.name?.length > 0 && + submission.gamesOfTheYear?.length > 0 && + (!hasGiveaway || submission.enteredGiveaway != null) && + Object.values(inputs).every(([value]) => value === '') + +interface SubmissionState { + submission: Submission + setSubmission: (submission: Submission) => void + inputs: SubmissionInputsState + isDirty: boolean + isValid: boolean + handleSubmit: () => void +} +export const useSubmissionForm = ( + handleDone: () => void, + handleError: (error: any) => void, +): SubmissionState => { + const queryClient = useQueryClient() + const { id } = useSubmissionIds() + const { data: initialSubmission } = useSubmission(id) + const { properties } = useProperties() + const [submission, setSubmission] = useState(initialSubmission) + const inputs: SubmissionInputsState = { + goty: useState(''), + mostDisappointing: useState(''), + mostAnticipated: useState(''), + bestOldGame: useState(''), + } + const isDirty = + !isEqual(submission, initialSubmission) || + Object.values(inputs).some(([value]) => value !== '') + useUnsavedChangesWarning(isDirty) + const upsertSubmissionMutation = useMutation({ + mutationFn: (newSubmission: Submission) => + id !== '' + ? SubmissionService.updateSubmission(newSubmission) + : SubmissionService.createSubmission(newSubmission), + onSuccess: (newSubmission) => { + queryClient.setQueryData(['submission', id], newSubmission) + handleDone() + }, + onError: (error) => { + handleError(error) + }, + }) + useEffect(() => { + setSubmission(initialSubmission) + }, [initialSubmission, setSubmission]) + const handleSubmit = useCallback(() => { + upsertSubmissionMutation.mutate(submission) + }, [upsertSubmissionMutation, submission]) + return { + submission, + setSubmission, + inputs, + isDirty, + isValid: submissionIsValid(submission, properties.hasGiveaway, inputs), + handleSubmit, + } +} diff --git a/goty-client/src/util/useUnsavedChangesWarning.ts b/goty-client/src/util/useUnsavedChangesWarning.ts index 90740cf..141da22 100644 --- a/goty-client/src/util/useUnsavedChangesWarning.ts +++ b/goty-client/src/util/useUnsavedChangesWarning.ts @@ -1,9 +1,9 @@ import { useEffect } from 'react' -export const useUnsavedChangesWarning = (hasUnsavedChanges: boolean) => { +export const useUnsavedChangesWarning = (isDirty: boolean) => { useEffect(() => { const beforeUnload = (e: any) => { - if (hasUnsavedChanges) { + if (isDirty) { e.preventDefault() e.returnValue = true } @@ -12,5 +12,5 @@ export const useUnsavedChangesWarning = (hasUnsavedChanges: boolean) => { return () => { window.removeEventListener('beforeunload', beforeUnload) } - }, [hasUnsavedChanges]) + }, [isDirty]) }