253 lines
7.9 KiB
TypeScript
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
|