RecipeEdit page basically working.

This commit is contained in:
Jesse Brault 2024-08-17 11:16:15 -05:00
parent 56dffa2ee8
commit af683d9ee9
9 changed files with 205 additions and 46 deletions

View File

@ -55,7 +55,7 @@ const getRecipeInfos = async ({
owner,
isPublic,
starCount,
mainImage: toImageView(rawMainImage),
mainImage: rawMainImage !== null ? toImageView(rawMainImage) : null,
slug
})
)

View File

@ -14,7 +14,7 @@ export interface RawFullRecipeView {
owner: UserInfoView
starCount: number
viewerCount: number
mainImage: RawImageView
mainImage: RawImageView | null
isPublic: boolean
}
@ -35,7 +35,7 @@ interface FullRecipeView {
owner: UserInfoView
starCount: number
viewerCount: number
mainImage: ImageView
mainImage: ImageView | null
isPublic: boolean
}
@ -71,7 +71,7 @@ export const toFullRecipeView = ({
owner,
starCount,
viewerCount,
mainImage: toImageView(rawMainImage),
mainImage: rawMainImage !== null ? toImageView(rawMainImage) : null,
isPublic
})

View File

@ -12,7 +12,7 @@ export interface RawRecipeInfoView {
owner: UserInfoView
isPublic: boolean
starCount: number
mainImage: RawImageView
mainImage: RawImageView | null
slug: string
}
@ -26,7 +26,7 @@ interface RecipeInfoView {
owner: UserInfoView
isPublic: boolean
starCount: number
mainImage: ImageView
mainImage: ImageView | null
slug: string
}

View File

@ -0,0 +1,42 @@
import { FullRecipeViewWithRawText } from './FullRecipeView'
export interface MainImageUpdateSpec {
username: string
filename: string
}
interface UpdateRecipeSpec {
title: string
preparationTime: number | null
cookingTime: number | null
totalTime: number | null
rawText: string
isPublic: boolean
mainImage: MainImageUpdateSpec | null
}
export const fromFullRecipeView = ({
title,
preparationTime,
cookingTime,
totalTime,
rawText,
isPublic,
mainImage
}: FullRecipeViewWithRawText): UpdateRecipeSpec => ({
title,
preparationTime,
cookingTime,
totalTime,
rawText,
isPublic,
mainImage:
mainImage !== null
? {
username: mainImage.owner.username,
filename: mainImage.filename
}
: null
})
export default UpdateRecipeSpec

36
src/api/updateRecipe.ts Normal file
View File

@ -0,0 +1,36 @@
import { ApiError } from './ApiError'
import ExpiredTokenError from './ExpiredTokenError'
import {
GetRecipeViewWithRawText,
RawGetRecipeViewWithRawText,
toGetRecipeViewWithRawText
} from './types/GetRecipeView'
import UpdateRecipeSpec from './types/UpdateRecipeSpec'
export interface UpdateRecipeDeps {
spec: UpdateRecipeSpec
token: string
username: string
slug: string
}
const updateRecipe = async ({ spec, token, username, slug }: UpdateRecipeDeps): Promise<GetRecipeViewWithRawText> => {
const headers = new Headers()
headers.set('Authorization', `Bearer ${token}`)
headers.set('Content-type', 'application/json')
const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}`, {
headers,
method: 'POST',
mode: 'cors',
body: JSON.stringify(spec)
})
if (response.ok) {
return toGetRecipeViewWithRawText((await response.json()) as RawGetRecipeViewWithRawText)
} else if (response.status === 401) {
throw new ExpiredTokenError()
} else {
throw new ApiError(response.status, response.statusText)
}
}
export default updateRecipe

View File

@ -1,8 +1,10 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import { PropsWithChildren, useEffect, useState } from 'react'
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 '../../auth'
import classes from './edit-recipe.module.css'
@ -87,9 +89,14 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
const auth = useAuth()
const navigate = useNavigate()
if (auth.token === null) {
navigate({ to: '/login', search: { reason: 'NOT_LOGGED_IN', redirect: `/recipes/${username}/${slug}/edit` } })
}
// useEffect(() => {
// if (auth.token === null) {
// navigate({
// to: '/login',
// search: { reason: 'NOT_LOGGED_IN', redirect: `/recipes/${username}/${slug}/edit` }
// })
// }
// }, [auth.token, navigate, username, slug])
const queryClient = useQueryClient()
@ -109,30 +116,82 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
)
const [isOwner, setIsOwner] = useState(false)
const [title, setTitle] = useState('')
const [mSlug, setMSlug] = useState('')
const [preparationTime, setPreparationTime] = useState<number | null>(null)
const [cookingTime, setCookingTime] = useState<number | null>(null)
const [totalTime, setTotalTime] = useState<number | null>(null)
const [recipeText, setRecipeText] = useState('')
const [spec, setSpec] = useState<UpdateRecipeSpec>({
title: '',
preparationTime: null,
cookingTime: null,
totalTime: null,
rawText: '',
mainImage: null,
isPublic: false
})
useEffect(() => {
if (recipeQuery.isSuccess) {
const { isOwner, recipe } = recipeQuery.data
if (!isOwner) {
setIsOwner(false)
} else {
setIsOwner(true)
setTitle(recipe.title)
setMSlug(recipe.slug)
setPreparationTime(recipe.preparationTime)
setCookingTime(recipe.cookingTime)
setTotalTime(recipe.totalTime)
setRecipeText(recipe.rawText)
setIsOwner(isOwner ?? false)
if (isOwner) {
setSpec(fromFullRecipeView(recipe))
}
}
}, [recipeQuery.isSuccess, recipeQuery.data])
const mutation = useMutation(
{
mutationFn: () => {
if (auth.token !== null) {
return updateRecipe({
spec,
token: auth.token,
username,
slug
})
} else {
return Promise.reject('Must be logged in.')
}
},
onSuccess: data => {
setIsOwner(data.isOwner ?? false)
setSpec(fromFullRecipeView(data.recipe))
queryClient.invalidateQueries({
queryKey: ['recipes', username, slug]
})
}
},
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.isLoading) {
return 'Loading...'
} else if (recipeQuery.isError) {
@ -153,30 +212,45 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
<div className={classes.articleContainer}>
<article>
<h1>Edit Recipe</h1>
<form className={classes.editForm}>
<form className={classes.editForm} onSubmit={onSubmit}>
<Control id="title" displayName="Title">
<TextInput id="title" value={title} setValue={setTitle} />
</Control>
<Control id="slug" displayName="Slug">
<TextInput id="slug" value={mSlug} setValue={setMSlug} />
<TextInput id="title" value={spec.title} setValue={getSetSpecText('title')} />
</Control>
<Control id="preparation-time" displayName="Preparation Time (in minutes)">
<TimeInput id="preparation-time" value={preparationTime} setValue={setPreparationTime} />
<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={cookingTime} setValue={setCookingTime} />
<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={totalTime} setValue={setTotalTime} />
<TimeInput id="total-time" value={spec.totalTime} setValue={getSetTimeSpec('totalTime')} />
</Control>
<Control id="recipe-text" displayName="Recipe Text">
<textarea value={recipeText} onChange={e => setRecipeText(e.target.value)} />
<textarea 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>

View File

@ -130,17 +130,17 @@ const Recipe = ({ username, slug }: RecipeProps) => {
const mainImageQuery = useQuery(
{
enabled: recipeQuery.isSuccess,
enabled: recipeQuery.isSuccess && recipeQuery.data!.recipe.mainImage !== null,
queryKey: [
'images',
recipeQuery.data?.recipe.mainImage.owner.username,
recipeQuery.data?.recipe.mainImage.filename
recipeQuery.data?.recipe.mainImage?.owner.username,
recipeQuery.data?.recipe.mainImage?.filename
],
queryFn: ({ signal }) =>
getImage({
accessToken: authContext.token,
signal,
url: recipeQuery.data!.recipe.mainImage.url
url: recipeQuery.data!.recipe.mainImage!.url
})
},
queryClient

View File

@ -33,17 +33,18 @@ const Recipes = () => {
data !== undefined
? data.content.map(recipeInfoView => {
return {
enabled: recipeInfoView.mainImage !== null,
queryKey: [
'images',
recipeInfoView.mainImage.owner.username,
recipeInfoView.mainImage.filename
recipeInfoView.mainImage?.owner.username,
recipeInfoView.mainImage?.filename
],
queryFn: async ({ signal }: any) => {
// any needed in the params
const imgUrl = await getImage({
accessToken: token,
signal,
url: recipeInfoView.mainImage.url
url: recipeInfoView.mainImage!.url
})
return {
slug: recipeInfoView.slug,
@ -83,7 +84,7 @@ const Recipes = () => {
return slugAndImgUrl !== undefined && slugAndImgUrl.slug === view.slug
})?.data!.imgUrl ?? '' // hacky workaround. should pass a kind of <Image> child which loads its own data
}
mainImageAlt={view.mainImage.alt ? view.mainImage.alt : undefined}
mainImageAlt={view.mainImage?.alt ? view.mainImage.alt : undefined}
starCount={view.starCount}
isPublic={view.isPublic}
/>

View File

@ -62,7 +62,13 @@ const Login = () => {
export const Route = createFileRoute('/login')({
validateSearch: z.object({
reason: z
.enum(['INVALID_REFRESH_TOKEN', 'EXPIRED_REFRESH_TOKEN', 'NO_REFRESH_TOKEN', 'NOT_LOGGED_IN'])
.enum([
'INVALID_REFRESH_TOKEN',
'EXPIRED_REFRESH_TOKEN',
'NO_REFRESH_TOKEN',
'NOT_LOGGED_IN',
'EXPIRED_ACCESS_TOKEN'
])
.optional(),
redirect: z.string().optional().catch('')
}),