Basic recipe editing page.
This commit is contained in:
parent
e328b64043
commit
8ce916731f
@ -7,9 +7,9 @@ export interface RawFullRecipeView {
|
||||
modified: string | null
|
||||
slug: string
|
||||
title: string
|
||||
preparationTime: number
|
||||
cookingTime: number
|
||||
totalTime: number
|
||||
preparationTime: number | null
|
||||
cookingTime: number | null
|
||||
totalTime: number | null
|
||||
text: string
|
||||
owner: UserInfoView
|
||||
starCount: number
|
||||
@ -24,9 +24,9 @@ interface FullRecipeView {
|
||||
modified: Date | null
|
||||
slug: string
|
||||
title: string
|
||||
preparationTime: number
|
||||
cookingTime: number
|
||||
totalTime: number
|
||||
preparationTime: number | null
|
||||
cookingTime: number | null
|
||||
totalTime: number | null
|
||||
text: string
|
||||
owner: UserInfoView
|
||||
starCount: number
|
||||
|
186
src/pages/edit-recipe/EditRecipe.tsx
Normal file
186
src/pages/edit-recipe/EditRecipe.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { ApiError } from '../../api/ApiError'
|
||||
import getRecipe from '../../api/getRecipe'
|
||||
import { useAuth } from '../../auth'
|
||||
import classes from './edit-recipe.module.css'
|
||||
|
||||
interface ControlProps {
|
||||
id: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
const Control = ({ id, displayName, children }: PropsWithChildren<ControlProps>) => {
|
||||
return (
|
||||
<div className={classes.control}>
|
||||
<label htmlFor={id}>{displayName}</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TextInputProps = {
|
||||
id: string
|
||||
} & (NullableTextInputProps | NonNullableTextInputProps)
|
||||
|
||||
interface NullableTextInputProps {
|
||||
nullable: true
|
||||
value: string | null
|
||||
setValue(newValue: string | null): void
|
||||
}
|
||||
|
||||
interface NonNullableTextInputProps {
|
||||
nullable?: false
|
||||
value: string
|
||||
setValue(newValue: string): void
|
||||
}
|
||||
|
||||
const TextInput = ({ nullable, id, value, setValue }: TextInputProps) => {
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
value={value ?? ''}
|
||||
onChange={e => {
|
||||
if (nullable) {
|
||||
setValue(e.target.value === '' ? null : e.target.value)
|
||||
} else {
|
||||
setValue(e.target.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface TimeInputProps {
|
||||
id: string
|
||||
value: number | null
|
||||
setValue(newValue: number | null): void
|
||||
}
|
||||
|
||||
const TimeInput = ({ id, value, setValue }: TimeInputProps) => {
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
value={value ?? ''}
|
||||
onChange={e => {
|
||||
if (e.target.value === '') {
|
||||
setValue(null)
|
||||
} else {
|
||||
const parsed = parseInt(e.target.value)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
setValue(parsed)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface EditRecipeProps {
|
||||
username: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
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` } })
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const recipeQuery = useQuery(
|
||||
{
|
||||
queryKey: ['recipes', username, slug],
|
||||
queryFn: ({ signal }) =>
|
||||
getRecipe({
|
||||
authContext: auth,
|
||||
username,
|
||||
slug,
|
||||
abortSignal: signal
|
||||
})
|
||||
},
|
||||
queryClient
|
||||
)
|
||||
|
||||
const [isOwner, setIsOwner] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
const [mSlug, setMSlug] = useState('')
|
||||
const [preparationTime, setPreparationTime] = useState<number | null>(null)
|
||||
const [cookingTime, setCookingTime] = useState<number | null>(null)
|
||||
const [totalTime, setTotalTime] = useState<number | null>(null)
|
||||
const [recipeText, setRecipeText] = useState('')
|
||||
|
||||
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.text)
|
||||
}
|
||||
}
|
||||
}, [recipeQuery.isSuccess, recipeQuery.data])
|
||||
|
||||
if (recipeQuery.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 (!isOwner) {
|
||||
return 'You do not have permission to edit this recipe.'
|
||||
} else {
|
||||
return (
|
||||
<div className={classes.articleContainer}>
|
||||
<article>
|
||||
<h1>Edit Recipe</h1>
|
||||
<form className={classes.editForm}>
|
||||
<Control id="title" displayName="Title">
|
||||
<TextInput id="title" value={title} setValue={setTitle} />
|
||||
</Control>
|
||||
|
||||
<Control id="slug" displayName="Slug">
|
||||
<TextInput id="slug" value={mSlug} setValue={setMSlug} />
|
||||
</Control>
|
||||
|
||||
<Control id="preparation-time" displayName="Preparation Time (in minutes)">
|
||||
<TimeInput id="preparation-time" value={preparationTime} setValue={setPreparationTime} />
|
||||
</Control>
|
||||
|
||||
<Control id="cooking-time" displayName="Cooking Time (in minutes)">
|
||||
<TimeInput id="cooking-time" value={cookingTime} setValue={setCookingTime} />
|
||||
</Control>
|
||||
|
||||
<Control id="total-time" displayName="Total Time (in minutes)">
|
||||
<TimeInput id="total-time" value={totalTime} setValue={setTotalTime} />
|
||||
</Control>
|
||||
|
||||
<Control id="recipe-text" displayName="Recipe Text">
|
||||
<textarea value={recipeText} onChange={e => setRecipeText(e.target.value)} />
|
||||
</Control>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EditRecipe
|
14
src/pages/edit-recipe/edit-recipe.module.css
Normal file
14
src/pages/edit-recipe/edit-recipe.module.css
Normal file
@ -0,0 +1,14 @@
|
||||
.article-container {
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
column-gap: 5px;
|
||||
}
|
@ -61,7 +61,9 @@ const Login = () => {
|
||||
|
||||
export const Route = createFileRoute('/login')({
|
||||
validateSearch: z.object({
|
||||
reason: z.enum(['INVALID_REFRESH_TOKEN', 'EXPIRED_REFRESH_TOKEN', 'NO_REFRESH_TOKEN']).optional(),
|
||||
reason: z
|
||||
.enum(['INVALID_REFRESH_TOKEN', 'EXPIRED_REFRESH_TOKEN', 'NO_REFRESH_TOKEN', 'NOT_LOGGED_IN'])
|
||||
.optional(),
|
||||
redirect: z.string().optional().catch('')
|
||||
}),
|
||||
beforeLoad({ context, search }) {
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||
import EditRecipe from '../../../pages/edit-recipe/EditRecipe'
|
||||
|
||||
export const Route = createFileRoute('/recipes/$username/$slug/edit')({
|
||||
component: () => <div>Hello /recipes/$username/$slug/edit!</div>
|
||||
component: () => {
|
||||
const { username, slug } = useParams({ from: '/recipes/$username/$slug/edit' })
|
||||
return <EditRecipe {...{ username, slug }} />
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user