From af683d9ee93cea3394b95da1072d530705c1460e Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Sat, 17 Aug 2024 11:16:15 -0500 Subject: [PATCH] RecipeEdit page basically working. --- src/api/getRecipeInfos.ts | 2 +- src/api/types/FullRecipeView.ts | 6 +- src/api/types/RecipeInfoView.ts | 4 +- src/api/types/UpdateRecipeSpec.ts | 42 +++++++++ src/api/updateRecipe.ts | 36 +++++++ src/pages/edit-recipe/EditRecipe.tsx | 136 +++++++++++++++++++++------ src/pages/recipe/Recipe.tsx | 8 +- src/pages/recipes/Recipes.tsx | 9 +- src/routes/login.tsx | 8 +- 9 files changed, 205 insertions(+), 46 deletions(-) create mode 100644 src/api/types/UpdateRecipeSpec.ts create mode 100644 src/api/updateRecipe.ts diff --git a/src/api/getRecipeInfos.ts b/src/api/getRecipeInfos.ts index 309922e..8124575 100644 --- a/src/api/getRecipeInfos.ts +++ b/src/api/getRecipeInfos.ts @@ -55,7 +55,7 @@ const getRecipeInfos = async ({ owner, isPublic, starCount, - mainImage: toImageView(rawMainImage), + mainImage: rawMainImage !== null ? toImageView(rawMainImage) : null, slug }) ) diff --git a/src/api/types/FullRecipeView.ts b/src/api/types/FullRecipeView.ts index a252bd4..9af9bc7 100644 --- a/src/api/types/FullRecipeView.ts +++ b/src/api/types/FullRecipeView.ts @@ -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 }) diff --git a/src/api/types/RecipeInfoView.ts b/src/api/types/RecipeInfoView.ts index 79a6308..020509a 100644 --- a/src/api/types/RecipeInfoView.ts +++ b/src/api/types/RecipeInfoView.ts @@ -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 } diff --git a/src/api/types/UpdateRecipeSpec.ts b/src/api/types/UpdateRecipeSpec.ts new file mode 100644 index 0000000..3b76b88 --- /dev/null +++ b/src/api/types/UpdateRecipeSpec.ts @@ -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 diff --git a/src/api/updateRecipe.ts b/src/api/updateRecipe.ts new file mode 100644 index 0000000..0829989 --- /dev/null +++ b/src/api/updateRecipe.ts @@ -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 => { + 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 diff --git a/src/pages/edit-recipe/EditRecipe.tsx b/src/pages/edit-recipe/EditRecipe.tsx index 5e776b3..276a770 100644 --- a/src/pages/edit-recipe/EditRecipe.tsx +++ b/src/pages/edit-recipe/EditRecipe.tsx @@ -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(null) - const [cookingTime, setCookingTime] = useState(null) - const [totalTime, setTotalTime] = useState(null) - const [recipeText, setRecipeText] = useState('') + + const [spec, setSpec] = useState({ + 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 = 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 => + (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) => {

Edit Recipe

-
+ - - - - - + - + - + - + -