199 lines
6.5 KiB
TypeScript
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
|