meals-made-easy-app/src/pages/edit-recipe/EditRecipe.tsx

253 lines
7.9 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ChangeEventHandler, FormEventHandler, PropsWithChildren, useEffect, useState } from 'react'
import { ApiError } from '../../api/ApiError'
import getRecipe from '../../api/getRecipe'
import UpdateRecipeSpec, { fromFullRecipeView } from '../../api/types/UpdateRecipeSpec'
import updateRecipe from '../../api/updateRecipe'
import { useAuth } from '../../AuthProvider'
import { useRefresh } from '../../RefreshProvider'
import classes from './edit-recipe.module.css'
interface ControlProps {
id: string
displayName: string
}
const Control = ({ id, displayName, children }: PropsWithChildren<ControlProps>) => {
return (
<div className={classes.control}>
<label htmlFor={id}>{displayName}</label>
{children}
</div>
)
}
type TextInputProps = {
id: string
} & (NullableTextInputProps | NonNullableTextInputProps)
interface NullableTextInputProps {
nullable: true
value: string | null
setValue(newValue: string | null): void
}
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 {
username: string
slug: string
}
const EditRecipe = ({ username, slug }: EditRecipeProps) => {
const { accessToken } = useAuth()
const refresh = useRefresh()
const queryClient = useQueryClient()
const [spec, setSpec] = useState<UpdateRecipeSpec>({
title: '',
preparationTime: null,
cookingTime: null,
totalTime: null,
rawText: '',
mainImage: null,
isPublic: false
})
const recipeQuery = useQuery(
{
queryKey: ['recipes', username, slug],
queryFn: ({ signal }) =>
getRecipe({
accessToken,
includeRawText: true,
refresh,
slug,
signal,
username
})
},
queryClient
)
useEffect(() => {
if (recipeQuery.isSuccess) {
setSpec(fromFullRecipeView(recipeQuery.data.recipe))
}
}, [recipeQuery.isSuccess, recipeQuery.data])
const mutation = useMutation(
{
mutationFn: () => {
if (accessToken !== null) {
return updateRecipe({
spec,
accessToken,
refresh,
username,
slug
})
} else {
return Promise.reject('Must be logged in.')
}
},
onSuccess: data => {
console.log(data)
setSpec(fromFullRecipeView(data.recipe))
queryClient.setQueryData(['recipes', username, slug], data)
}
},
queryClient
)
const onSubmit: FormEventHandler<HTMLFormElement> = e => {
e.preventDefault()
mutation.mutate()
return false
}
const getSetSpecText =
(prop: keyof UpdateRecipeSpec) =>
(value: string): void => {
const next = { ...spec }
;(next as any)[prop] = value
setSpec(next)
}
const getSetTimeSpec =
(prop: keyof UpdateRecipeSpec) =>
(value: number | null): void => {
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) {
console.log('we are pending')
return 'Loading...'
} else if (recipeQuery.isError) {
console.log('we had an error')
const { error } = recipeQuery
if (error instanceof ApiError) {
if (error.status === 404) {
return 'No such recipe.'
} else {
return `ApiError: ${error.status} ${error.message}`
}
} else {
return `Error: ${error.name} ${error.message}`
}
} else if (recipeQuery.isSuccess && !recipeQuery.data.isOwner) {
console.log('we are not the owner')
return 'You do not have permission to edit this recipe.'
} else {
console.log('doing whole page')
return (
<div className={classes.articleContainer}>
<article>
<h1>Edit Recipe</h1>
<form className={classes.editForm} onSubmit={onSubmit}>
<Control id="title" displayName="Title">
<TextInput id="title" value={spec.title} setValue={getSetSpecText('title')} />
</Control>
<Control id="preparation-time" displayName="Preparation Time (in minutes)">
<TimeInput
id="preparation-time"
value={spec.preparationTime}
setValue={getSetTimeSpec('preparationTime')}
/>
</Control>
<Control id="cooking-time" displayName="Cooking Time (in minutes)">
<TimeInput
id="cooking-time"
value={spec.cookingTime}
setValue={getSetTimeSpec('cookingTime')}
/>
</Control>
<Control id="total-time" displayName="Total Time (in minutes)">
<TimeInput id="total-time" value={spec.totalTime} setValue={getSetTimeSpec('totalTime')} />
</Control>
<Control id="recipe-text" displayName="Recipe Text">
<textarea
id="recipe-text"
value={spec.rawText}
onChange={getSetSpecTextAsHandler('rawText')}
/>
</Control>
<div className={classes.submitContainer}>
<input type="submit" />
{mutation.isPending
? 'Saving...'
: mutation.isSuccess
? 'Saved!'
: mutation.isError
? `Error! ${mutation.error}`
: null}
</div>
</form>
</article>
</div>
)
}
}
export default EditRecipe