diff --git a/src/api/addStar.ts b/src/api/addStar.ts new file mode 100644 index 0000000..770faf9 --- /dev/null +++ b/src/api/addStar.ts @@ -0,0 +1,25 @@ +import { ApiError } from './ApiError' +import ExpiredTokenError from './ExpiredTokenError' + +export interface AddStarDeps { + token: string + username: string + slug: string +} + +const addStar = async ({ slug, token, username }: AddStarDeps): Promise => { + const headers = new Headers() + headers.set('Authorization', `Bearer ${token}`) + const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, { + headers, + method: 'POST', + mode: 'cors' + }) + if (response.status === 401) { + throw new ExpiredTokenError() + } else if (!response.ok) { + throw new ApiError(response.status, response.statusText) + } +} + +export default addStar diff --git a/src/api/removeStar.ts b/src/api/removeStar.ts new file mode 100644 index 0000000..a851647 --- /dev/null +++ b/src/api/removeStar.ts @@ -0,0 +1,25 @@ +import { ApiError } from './ApiError' +import ExpiredTokenError from './ExpiredTokenError' + +export interface RemoveStarDeps { + token: string + username: string + slug: string +} + +const removeStar = async ({ token, username, slug }: RemoveStarDeps) => { + const headers = new Headers() + headers.set('Authorization', `Bearer ${token}`) + const response = await fetch(import.meta.env.VITE_MME_API_URL + `/recipes/${username}/${slug}/star`, { + headers, + method: 'DELETE', + mode: 'cors' + }) + if (response.status === 401) { + throw new ExpiredTokenError() + } else if (!response.ok) { + throw new ApiError(response.status, response.statusText) + } +} + +export default removeStar diff --git a/src/api/types/FullRecipeView.ts b/src/api/types/FullRecipeView.ts index 0e3388a..d28ab76 100644 --- a/src/api/types/FullRecipeView.ts +++ b/src/api/types/FullRecipeView.ts @@ -13,6 +13,7 @@ export interface RawFullRecipeView { text: string owner: UserInfoView starCount: number + isStarred: boolean | null viewerCount: number mainImage: RawImageView isPublic: boolean @@ -30,6 +31,7 @@ interface FullRecipeView { text: string owner: UserInfoView starCount: number + isStarred: boolean | null viewerCount: number mainImage: ImageView isPublic: boolean @@ -47,6 +49,7 @@ export const toFullRecipeView = ({ text, owner, starCount, + isStarred, viewerCount, mainImage: rawMainImage, isPublic @@ -62,6 +65,7 @@ export const toFullRecipeView = ({ text, owner, starCount, + isStarred, viewerCount, mainImage: toImageView(rawMainImage), isPublic diff --git a/src/pages/recipe/Recipe.tsx b/src/pages/recipe/Recipe.tsx index 86295f9..4fad19b 100644 --- a/src/pages/recipe/Recipe.tsx +++ b/src/pages/recipe/Recipe.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { QueryObserverSuccessResult, useQuery, useQueryClient } from '@tanstack/react-query' +import { QueryObserverSuccessResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ApiError } from '../../api/ApiError' import getImage from '../../api/getImage' import getRecipe from '../../api/getRecipe' @@ -8,6 +8,8 @@ import { useAuth } from '../../auth' import RecipeVisibilityIcon from '../../components/recipe-visibility-icon/RecipeVisibilityIcon' import UserIconAndName from '../../components/user-icon-and-name/UserIconAndName' import classes from './recipe.module.css' +import addStar from '../../api/addStar' +import removeStar from '../../api/removeStar' export interface RecipeProps { username: string @@ -20,7 +22,7 @@ const Recipe = ({ username, slug }: RecipeProps) => { const recipeQuery = useQuery( { - queryKey: ['recipe', username, slug], + queryKey: ['recipes', username, slug], queryFn: ({ signal: abortSignal }) => getRecipe({ abortSignal, @@ -46,6 +48,50 @@ const Recipe = ({ username, slug }: RecipeProps) => { queryClient ) + const addStarMutation = useMutation({ + mutationFn: () => { + if (authContext.token !== null) { + return addStar({ + token: authContext.token, + slug, + username + }) + } else { + return Promise.resolve() + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['recipes', username, slug] }) + } + }) + + const removeStarMutation = useMutation({ + mutationFn: () => { + if (authContext.token !== null) { + return removeStar({ + token: authContext.token, + slug, + username + }) + } else { + return Promise.resolve() + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['recipes', username, slug] }) + } + }) + + const onStarButtonClick = () => { + if (recipeQuery.isSuccess) { + if (recipeQuery.data.isStarred) { + removeStarMutation.mutate() + } else { + addStarMutation.mutate() + } + } + } + if (recipeQuery.isLoading || mainImageQuery.isLoading) { return 'Loading...' } else if (recipeQuery.isError) { @@ -73,9 +119,9 @@ const Recipe = ({ username, slug }: RecipeProps) => {

{recipe.title}

-