meals-made-easy-app/src/pages/recipe/Recipe.tsx
2024-08-14 20:44:31 -05:00

199 lines
6.5 KiB
TypeScript

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { QueryObserverSuccessResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import addStar from '../../api/addStar'
import { ApiError } from '../../api/ApiError'
import getImage from '../../api/getImage'
import getRecipe from '../../api/getRecipe'
import removeStar from '../../api/removeStar'
import GetRecipeView from '../../api/types/GetRecipeView'
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 { useNavigate } from '@tanstack/react-router'
interface EditButtonProps {
username: string
slug: string
}
const EditButton = ({ username, slug }: EditButtonProps) => {
const navigate = useNavigate()
return (
<button
className={classes.editButton}
onClick={() => navigate({ to: '/recipes/$username/$slug/edit', params: { username, slug } })}
>
<FontAwesomeIcon icon="pencil" className={classes.editIcon} size="sm" />
<span className={classes.buttonText}>Edit</span>
</button>
)
}
interface RecipeStarInfoProps {
starCount: number
}
const RecipeStarInfo = ({ starCount }: RecipeStarInfoProps) => {
return (
<div className={classes.starContainer}>
<FontAwesomeIcon icon="star" className={classes.star} size="sm" />
<span className={classes.buttonText}>{starCount}</span>
</div>
)
}
interface RecipeStarButtonProps {
username: string
slug: string
isStarred: boolean
starCount: number
}
const RecipeStarButton = ({ username, slug, isStarred, starCount }: RecipeStarButtonProps) => {
const authContext = useAuth()
const queryClient = useQueryClient()
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 onClick = () => {
if (isStarred) {
removeStarMutation.mutate()
} else {
addStarMutation.mutate()
}
}
return (
<button className={classes.starContainer} onClick={onClick}>
<FontAwesomeIcon icon="star" className={classes.star} size="sm" />
<span className={classes.buttonText}>{isStarred ? 'Starred' : 'Star'}</span>
<span className={classes.starCount}>{starCount}</span>
</button>
)
}
export interface RecipeProps {
username: string
slug: string
}
const Recipe = ({ username, slug }: RecipeProps) => {
const authContext = useAuth()
const queryClient = useQueryClient()
const recipeQuery = useQuery(
{
queryKey: ['recipes', username, slug],
queryFn: ({ signal: abortSignal }) =>
getRecipe({
abortSignal,
authContext,
username,
slug
})
},
queryClient
)
const mainImageQuery = useQuery(
{
enabled: recipeQuery.isSuccess,
queryKey: [
'images',
recipeQuery.data?.recipe.mainImage.owner.username,
recipeQuery.data?.recipe.mainImage.filename
],
queryFn: ({ signal }) =>
getImage({
accessToken: authContext.token,
signal,
url: recipeQuery.data!.recipe.mainImage.url
})
},
queryClient
)
if (recipeQuery.isLoading || mainImageQuery.isLoading) {
return 'Loading...'
} else if (recipeQuery.isError) {
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 (mainImageQuery.isError) {
const { error } = mainImageQuery
return `Error: ${error.name} ${error.message}`
}
const { data: getRecipeView } = recipeQuery as QueryObserverSuccessResult<GetRecipeView>
const { data: mainImageUrl } = mainImageQuery as QueryObserverSuccessResult<string>
const { recipe, isStarred, isOwner } = getRecipeView
return (
<div className={classes.fullRecipeContainer}>
<article className={classes.fullRecipe}>
<div className={classes.info}>
<div className={classes.infoRow}>
<h1 className={classes.recipeTitle}>{recipe.title}</h1>
<div className={classes.infoButtons}>
{isStarred !== null ? (
<RecipeStarButton starCount={recipe.starCount} {...{ isStarred, username, slug }} />
) : (
<RecipeStarInfo starCount={recipe.starCount} />
)}
{isOwner ? <EditButton {...{ username, slug }} /> : null}
</div>
</div>
<div className={classes.infoRow}>
<UserIconAndName username={recipe.owner.username} />
<RecipeVisibilityIcon isPublic={recipe.isPublic} />
</div>
</div>
<img src={mainImageUrl} className={classes.mainImage} />
<div dangerouslySetInnerHTML={{ __html: recipe.text }} />
</article>
</div>
)
}
export default Recipe