RecipeEdit page basically working.
This commit is contained in:
parent
56dffa2ee8
commit
af683d9ee9
@ -55,7 +55,7 @@ const getRecipeInfos = async ({
|
|||||||
owner,
|
owner,
|
||||||
isPublic,
|
isPublic,
|
||||||
starCount,
|
starCount,
|
||||||
mainImage: toImageView(rawMainImage),
|
mainImage: rawMainImage !== null ? toImageView(rawMainImage) : null,
|
||||||
slug
|
slug
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,7 @@ export interface RawFullRecipeView {
|
|||||||
owner: UserInfoView
|
owner: UserInfoView
|
||||||
starCount: number
|
starCount: number
|
||||||
viewerCount: number
|
viewerCount: number
|
||||||
mainImage: RawImageView
|
mainImage: RawImageView | null
|
||||||
isPublic: boolean
|
isPublic: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ interface FullRecipeView {
|
|||||||
owner: UserInfoView
|
owner: UserInfoView
|
||||||
starCount: number
|
starCount: number
|
||||||
viewerCount: number
|
viewerCount: number
|
||||||
mainImage: ImageView
|
mainImage: ImageView | null
|
||||||
isPublic: boolean
|
isPublic: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export const toFullRecipeView = ({
|
|||||||
owner,
|
owner,
|
||||||
starCount,
|
starCount,
|
||||||
viewerCount,
|
viewerCount,
|
||||||
mainImage: toImageView(rawMainImage),
|
mainImage: rawMainImage !== null ? toImageView(rawMainImage) : null,
|
||||||
isPublic
|
isPublic
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export interface RawRecipeInfoView {
|
|||||||
owner: UserInfoView
|
owner: UserInfoView
|
||||||
isPublic: boolean
|
isPublic: boolean
|
||||||
starCount: number
|
starCount: number
|
||||||
mainImage: RawImageView
|
mainImage: RawImageView | null
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ interface RecipeInfoView {
|
|||||||
owner: UserInfoView
|
owner: UserInfoView
|
||||||
isPublic: boolean
|
isPublic: boolean
|
||||||
starCount: number
|
starCount: number
|
||||||
mainImage: ImageView
|
mainImage: ImageView | null
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
src/api/types/UpdateRecipeSpec.ts
Normal file
42
src/api/types/UpdateRecipeSpec.ts
Normal 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
36
src/api/updateRecipe.ts
Normal 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
|
@ -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 { 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 { ApiError } from '../../api/ApiError'
|
||||||
import getRecipe from '../../api/getRecipe'
|
import getRecipe from '../../api/getRecipe'
|
||||||
|
import UpdateRecipeSpec, { fromFullRecipeView } from '../../api/types/UpdateRecipeSpec'
|
||||||
|
import updateRecipe from '../../api/updateRecipe'
|
||||||
import { useAuth } from '../../auth'
|
import { useAuth } from '../../auth'
|
||||||
import classes from './edit-recipe.module.css'
|
import classes from './edit-recipe.module.css'
|
||||||
|
|
||||||
@ -87,9 +89,14 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
|||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
if (auth.token === null) {
|
// useEffect(() => {
|
||||||
navigate({ to: '/login', search: { reason: 'NOT_LOGGED_IN', redirect: `/recipes/${username}/${slug}/edit` } })
|
// 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()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@ -109,30 +116,82 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const [isOwner, setIsOwner] = useState(false)
|
const [isOwner, setIsOwner] = useState(false)
|
||||||
const [title, setTitle] = useState('')
|
|
||||||
const [mSlug, setMSlug] = useState('')
|
const [spec, setSpec] = useState<UpdateRecipeSpec>({
|
||||||
const [preparationTime, setPreparationTime] = useState<number | null>(null)
|
title: '',
|
||||||
const [cookingTime, setCookingTime] = useState<number | null>(null)
|
preparationTime: null,
|
||||||
const [totalTime, setTotalTime] = useState<number | null>(null)
|
cookingTime: null,
|
||||||
const [recipeText, setRecipeText] = useState('')
|
totalTime: null,
|
||||||
|
rawText: '',
|
||||||
|
mainImage: null,
|
||||||
|
isPublic: false
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recipeQuery.isSuccess) {
|
if (recipeQuery.isSuccess) {
|
||||||
const { isOwner, recipe } = recipeQuery.data
|
const { isOwner, recipe } = recipeQuery.data
|
||||||
if (!isOwner) {
|
setIsOwner(isOwner ?? false)
|
||||||
setIsOwner(false)
|
if (isOwner) {
|
||||||
} else {
|
setSpec(fromFullRecipeView(recipe))
|
||||||
setIsOwner(true)
|
|
||||||
setTitle(recipe.title)
|
|
||||||
setMSlug(recipe.slug)
|
|
||||||
setPreparationTime(recipe.preparationTime)
|
|
||||||
setCookingTime(recipe.cookingTime)
|
|
||||||
setTotalTime(recipe.totalTime)
|
|
||||||
setRecipeText(recipe.rawText)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [recipeQuery.isSuccess, recipeQuery.data])
|
}, [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) {
|
if (recipeQuery.isLoading) {
|
||||||
return 'Loading...'
|
return 'Loading...'
|
||||||
} else if (recipeQuery.isError) {
|
} else if (recipeQuery.isError) {
|
||||||
@ -153,30 +212,45 @@ const EditRecipe = ({ username, slug }: EditRecipeProps) => {
|
|||||||
<div className={classes.articleContainer}>
|
<div className={classes.articleContainer}>
|
||||||
<article>
|
<article>
|
||||||
<h1>Edit Recipe</h1>
|
<h1>Edit Recipe</h1>
|
||||||
<form className={classes.editForm}>
|
<form className={classes.editForm} onSubmit={onSubmit}>
|
||||||
<Control id="title" displayName="Title">
|
<Control id="title" displayName="Title">
|
||||||
<TextInput id="title" value={title} setValue={setTitle} />
|
<TextInput id="title" value={spec.title} setValue={getSetSpecText('title')} />
|
||||||
</Control>
|
|
||||||
|
|
||||||
<Control id="slug" displayName="Slug">
|
|
||||||
<TextInput id="slug" value={mSlug} setValue={setMSlug} />
|
|
||||||
</Control>
|
</Control>
|
||||||
|
|
||||||
<Control id="preparation-time" displayName="Preparation Time (in minutes)">
|
<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>
|
||||||
|
|
||||||
<Control id="cooking-time" displayName="Cooking Time (in minutes)">
|
<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>
|
||||||
|
|
||||||
<Control id="total-time" displayName="Total Time (in minutes)">
|
<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>
|
||||||
|
|
||||||
<Control id="recipe-text" displayName="Recipe Text">
|
<Control id="recipe-text" displayName="Recipe Text">
|
||||||
<textarea value={recipeText} onChange={e => setRecipeText(e.target.value)} />
|
<textarea value={spec.rawText} onChange={getSetSpecTextAsHandler('rawText')} />
|
||||||
</Control>
|
</Control>
|
||||||
|
|
||||||
|
<div className={classes.submitContainer}>
|
||||||
|
<input type="submit" />
|
||||||
|
{mutation.isPending
|
||||||
|
? 'Saving...'
|
||||||
|
: mutation.isSuccess
|
||||||
|
? 'Saved!'
|
||||||
|
: mutation.isError
|
||||||
|
? `Error! ${mutation.error}`
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
@ -130,17 +130,17 @@ const Recipe = ({ username, slug }: RecipeProps) => {
|
|||||||
|
|
||||||
const mainImageQuery = useQuery(
|
const mainImageQuery = useQuery(
|
||||||
{
|
{
|
||||||
enabled: recipeQuery.isSuccess,
|
enabled: recipeQuery.isSuccess && recipeQuery.data!.recipe.mainImage !== null,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'images',
|
'images',
|
||||||
recipeQuery.data?.recipe.mainImage.owner.username,
|
recipeQuery.data?.recipe.mainImage?.owner.username,
|
||||||
recipeQuery.data?.recipe.mainImage.filename
|
recipeQuery.data?.recipe.mainImage?.filename
|
||||||
],
|
],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
getImage({
|
getImage({
|
||||||
accessToken: authContext.token,
|
accessToken: authContext.token,
|
||||||
signal,
|
signal,
|
||||||
url: recipeQuery.data!.recipe.mainImage.url
|
url: recipeQuery.data!.recipe.mainImage!.url
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
queryClient
|
queryClient
|
||||||
|
@ -33,17 +33,18 @@ const Recipes = () => {
|
|||||||
data !== undefined
|
data !== undefined
|
||||||
? data.content.map(recipeInfoView => {
|
? data.content.map(recipeInfoView => {
|
||||||
return {
|
return {
|
||||||
|
enabled: recipeInfoView.mainImage !== null,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'images',
|
'images',
|
||||||
recipeInfoView.mainImage.owner.username,
|
recipeInfoView.mainImage?.owner.username,
|
||||||
recipeInfoView.mainImage.filename
|
recipeInfoView.mainImage?.filename
|
||||||
],
|
],
|
||||||
queryFn: async ({ signal }: any) => {
|
queryFn: async ({ signal }: any) => {
|
||||||
// any needed in the params
|
// any needed in the params
|
||||||
const imgUrl = await getImage({
|
const imgUrl = await getImage({
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
signal,
|
signal,
|
||||||
url: recipeInfoView.mainImage.url
|
url: recipeInfoView.mainImage!.url
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
slug: recipeInfoView.slug,
|
slug: recipeInfoView.slug,
|
||||||
@ -83,7 +84,7 @@ const Recipes = () => {
|
|||||||
return slugAndImgUrl !== undefined && slugAndImgUrl.slug === view.slug
|
return slugAndImgUrl !== undefined && slugAndImgUrl.slug === view.slug
|
||||||
})?.data!.imgUrl ?? '' // hacky workaround. should pass a kind of <Image> child which loads its own data
|
})?.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}
|
starCount={view.starCount}
|
||||||
isPublic={view.isPublic}
|
isPublic={view.isPublic}
|
||||||
/>
|
/>
|
||||||
|
@ -62,7 +62,13 @@ const Login = () => {
|
|||||||
export const Route = createFileRoute('/login')({
|
export const Route = createFileRoute('/login')({
|
||||||
validateSearch: z.object({
|
validateSearch: z.object({
|
||||||
reason: z
|
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(),
|
.optional(),
|
||||||
redirect: z.string().optional().catch('')
|
redirect: z.string().optional().catch('')
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user