EditRecipe using state and refs to track form data.
This commit is contained in:
parent
21c154ae47
commit
d6629e5176
@ -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}>
|
||||||
|
Loading…
Reference in New Issue
Block a user