EditRecipe using state and refs to track form data.

This commit is contained in:
Jesse Brault 2024-08-24 18:43:35 -05:00
parent 21c154ae47
commit d6629e5176

View File

@ -1,8 +1,17 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ChangeEventHandler, FormEventHandler, PropsWithChildren, useEffect, useState } from 'react' import {
ChangeEventHandler,
FormEventHandler,
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import { ApiError } from '../../api/ApiError' import { ApiError } from '../../api/ApiError'
import getRecipe from '../../api/getRecipe' import getRecipe from '../../api/getRecipe'
import UpdateRecipeSpec, { fromFullRecipeView } from '../../api/types/UpdateRecipeSpec' import { FullRecipeViewWithRawText } from '../../api/types/FullRecipeView'
import UpdateRecipeSpec, { MainImageUpdateSpec } from '../../api/types/UpdateRecipeSpec'
import updateRecipe from '../../api/updateRecipe' import updateRecipe from '../../api/updateRecipe'
import { useAuth } from '../../AuthProvider' import { useAuth } from '../../AuthProvider'
import { useRefresh } from '../../RefreshProvider' import { useRefresh } from '../../RefreshProvider'
@ -22,63 +31,18 @@ const Control = ({ id, displayName, children }: PropsWithChildren<ControlProps>)
) )
} }
type TextInputProps = { const getOnTimeChange =
id: string (setTime: (n: number | null) => void): ChangeEventHandler<HTMLInputElement> =>
} & (NullableTextInputProps | NonNullableTextInputProps) e => {
if (e.target.value === '') {
interface NullableTextInputProps { setTime(null)
nullable: true } else {
value: string | null const parsed = parseInt(e.target.value)
setValue(newValue: string | null): void if (!Number.isNaN(parsed)) {
} setTime(parsed)
}
interface NonNullableTextInputProps { }
nullable?: false }
value: string
setValue(newValue: string): void
}
const TextInput = ({ nullable, id, value, setValue }: TextInputProps) => {
return (
<input
id={id}
type="text"
value={value ?? ''}
onChange={e => {
if (nullable) {
setValue(e.target.value === '' ? null : e.target.value)
} else {
setValue(e.target.value)
}
}}
/>
)
}
interface TimeInputProps {
id: string
value: number | null
setValue(newValue: number | null): void
}
const TimeInput = ({ id, value, setValue }: TimeInputProps) => {
return (
<input
id={id}
value={value ?? ''}
onChange={e => {
if (e.target.value === '') {
setValue(null)
} else {
const parsed = parseInt(e.target.value)
if (!Number.isNaN(parsed)) {
setValue(parsed)
}
}
}}
/>
)
}
export interface EditRecipeProps { export interface EditRecipeProps {
username: string username: string
@ -89,15 +53,44 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
const { accessToken } = useAuth() const { accessToken } = useAuth()
const refresh = useRefresh() const refresh = useRefresh()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [spec, setSpec] = useState<UpdateRecipeSpec>({
title: '', const titleRef = useRef<HTMLInputElement | null>(null)
preparationTime: null, const [preparationTime, setPreparationTime] = useState<number | null>(null)
cookingTime: null, const [cookingTime, setCookingTime] = useState<number | null>(null)
totalTime: null, const [totalTime, setTotalTime] = useState<number | null>(null)
rawText: '', const recipeTextRef = useRef<HTMLTextAreaElement | null>(null)
mainImage: null, const isPublicRef = useRef<HTMLInputElement | null>(null)
isPublic: false const [mainImage, setMainImage] = useState<MainImageUpdateSpec | null>(null)
})
const onPreparationTimeChange = useCallback(getOnTimeChange(setPreparationTime), [setPreparationTime])
const onCookingTimeChange = useCallback(getOnTimeChange(setCookingTime), [setCookingTime])
const onTotalTimeChange = useCallback(getOnTimeChange(setTotalTime), [setTotalTime])
const setState = useCallback(
(recipe: FullRecipeViewWithRawText) => {
if (titleRef.current) {
titleRef.current.value = recipe.title
}
setPreparationTime(recipe.preparationTime)
setCookingTime(recipe.cookingTime)
setTotalTime(recipe.totalTime)
if (recipeTextRef.current) {
recipeTextRef.current.value = recipe.rawText
}
if (isPublicRef.current) {
isPublicRef.current.checked = recipe.isPublic
}
setMainImage(
recipe.mainImage
? {
username: recipe.mainImage.owner.username,
filename: recipe.mainImage?.filename
}
: null
)
},
[setPreparationTime, setCookingTime, setTotalTime, setMainImage]
)
const recipeQuery = useQuery( const recipeQuery = useQuery(
{ {
@ -117,16 +110,16 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
useEffect(() => { useEffect(() => {
if (recipeQuery.isSuccess) { if (recipeQuery.isSuccess) {
setSpec(fromFullRecipeView(recipeQuery.data.recipe)) setState(recipeQuery.data.recipe)
} }
}, [recipeQuery.isSuccess, recipeQuery.data]) }, [recipeQuery.isSuccess, setState, recipeQuery.data?.recipe])
const mutation = useMutation( const mutation = useMutation(
{ {
mutationFn: () => { mutationFn: (variables: { spec: UpdateRecipeSpec }) => {
if (accessToken !== null) { if (accessToken !== null) {
return updateRecipe({ return updateRecipe({
spec, spec: variables.spec,
accessToken, accessToken,
refresh, refresh,
username, username,
@ -137,49 +130,35 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
} }
}, },
onSuccess: data => { onSuccess: data => {
console.log(data) setState(data.recipe)
setSpec(fromFullRecipeView(data.recipe))
queryClient.setQueryData(['recipes', username, slug], data) queryClient.setQueryData(['recipes', username, slug], data)
} }
}, },
queryClient queryClient
) )
const onSubmit: FormEventHandler<HTMLFormElement> = e => { const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
e.preventDefault() e => {
mutation.mutate() e.preventDefault()
return false mutation.mutate({
} spec: {
title: titleRef.current!.value,
const getSetSpecText = preparationTime,
(prop: keyof UpdateRecipeSpec) => cookingTime,
(value: string): void => { totalTime,
const next = { ...spec } rawText: recipeTextRef.current!.value,
;(next as any)[prop] = value isPublic: isPublicRef.current!.checked,
setSpec(next) mainImage
} }
})
const getSetTimeSpec = return false
(prop: keyof UpdateRecipeSpec) => },
(value: number | null): void => { [mutation]
const next = { ...spec } )
;(next as any)[prop] = value
setSpec(next)
}
const getSetSpecTextAsHandler =
(prop: keyof UpdateRecipeSpec): ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> =>
(event): void => {
const next = { ...spec }
;(next as any)[prop] = event.target.value
setSpec(next)
}
if (recipeQuery.isPending) { if (recipeQuery.isPending) {
console.log('we are pending')
return 'Loading...' return 'Loading...'
} else if (recipeQuery.isError) { } else if (recipeQuery.isError) {
console.log('we had an error')
const { error } = recipeQuery const { error } = recipeQuery
if (error instanceof ApiError) { if (error instanceof ApiError) {
if (error.status === 404) { if (error.status === 404) {
@ -191,45 +170,50 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
return `Error: ${error.name} ${error.message}` return `Error: ${error.name} ${error.message}`
} }
} else if (recipeQuery.isSuccess && !recipeQuery.data.isOwner) { } else if (recipeQuery.isSuccess && !recipeQuery.data.isOwner) {
console.log('we are not the owner')
return 'You do not have permission to edit this recipe.' return 'You do not have permission to edit this recipe.'
} else { } else {
console.log('doing whole page')
return ( return (
<div className={classes.articleContainer}> <div className={classes.articleContainer}>
<article> <article>
<h1>Edit Recipe</h1> <h1>Edit Recipe</h1>
<form className={classes.editForm} onSubmit={onSubmit}> <form className={classes.editForm} onSubmit={onSubmit}>
<Control id="title" displayName="Title"> <Control id="title" displayName="Title">
<TextInput id="title" value={spec.title} setValue={getSetSpecText('title')} /> <input id="title" type="text" ref={titleRef} />
</Control> </Control>
<Control id="preparation-time" displayName="Preparation Time (in minutes)"> <Control id="preparation-time" displayName="Preparation Time (in minutes)">
<TimeInput <input
id="preparation-time" id="preparation-time"
value={spec.preparationTime} type="text"
setValue={getSetTimeSpec('preparationTime')} value={preparationTime?.toString() ?? ''}
onChange={onPreparationTimeChange}
/> />
</Control> </Control>
<Control id="cooking-time" displayName="Cooking Time (in minutes)"> <Control id="cooking-time" displayName="Cooking Time (in minutes)">
<TimeInput <input
id="cooking-time" id="cooking-time"
value={spec.cookingTime} type="text"
setValue={getSetTimeSpec('cookingTime')} value={cookingTime?.toString() ?? ''}
onChange={onCookingTimeChange}
/> />
</Control> </Control>
<Control id="total-time" displayName="Total Time (in minutes)"> <Control id="total-time" displayName="Total Time (in minutes)">
<TimeInput id="total-time" value={spec.totalTime} setValue={getSetTimeSpec('totalTime')} /> <input
id="total-time"
type="text"
value={totalTime?.toString() ?? ''}
onChange={onTotalTimeChange}
/>
</Control> </Control>
<Control id="recipe-text" displayName="Recipe Text"> <Control id="recipe-text" displayName="Recipe Text">
<textarea <textarea id="recipe-text" ref={recipeTextRef} />
id="recipe-text" </Control>
value={spec.rawText}
onChange={getSetSpecTextAsHandler('rawText')} <Control id="is-public" displayName="Is Public?">
/> <input id="is-public" type="checkbox" ref={isPublicRef} />
</Control> </Control>
<div className={classes.submitContainer}> <div className={classes.submitContainer}>