work-timer/app/routes/time-entries/new.tsx

124 lines
4 KiB
TypeScript
Raw Normal View History

2023-02-11 03:14:14 +01:00
import type { ActionArgs } from '@remix-run/node'
import { json, redirect } from '@remix-run/node'
import { Form, useActionData } from '@remix-run/react'
import * as React from 'react'
import z from 'zod'
import { createTimeEntry } from '~/models/timeEntry.server'
import { requireUserId } from '~/session.server'
export async function action({ request }: ActionArgs) {
const userId = await requireUserId(request)
const formData = await request.formData()
const description = formData.get('description')
const projectId = formData.get('projectId')
let startTime = formData.get('startTime')
let endTime = formData.get('endTime')
if (typeof description !== 'string' || description.length === 0) {
return json({ errors: { description: 'Description is required' } }, { status: 400 })
}
if (typeof projectId !== 'string' || projectId.length === 0) {
return json({ errors: { projectId: 'projectId is required' } }, { status: 400 })
}
if (typeof startTime !== 'string' || startTime.length === 0) {
return json({ errors: { startTime: 'startTime is required' } }, { status: 400 })
}
if (startTime && typeof startTime === 'string' && Number.isNaN(Date.parse(startTime))) {
return json({ errors: { startTime: 'startTime is invalid' } }, { status: 422 })
}
if (endTime && typeof endTime === 'string' && Number.isNaN(Date.parse(endTime))) {
return json({ errors: { endTime: 'endTime is invalid' } }, { status: 422 })
}
if (
startTime &&
endTime &&
typeof startTime === 'string' &&
typeof endTime === 'string' &&
new Date(startTime) > new Date(endTime)
) {
return json({ errors: { endTime: 'startTime must be before endTime' } }, { status: 422 })
}
const timeEntry = await createTimeEntry({
description,
startTime: new Date(startTime),
endTime: typeof endTime === 'string' ? new Date(endTime) : null,
userId,
projectId
})
return redirect(`/time-entries/${timeEntry.id}`)
}
export default function NewNotePage() {
const actionData = useActionData<typeof action>()
const titleRef = React.useRef<HTMLInputElement>(null)
const bodyRef = React.useRef<HTMLTextAreaElement>(null)
React.useEffect(() => {
if (actionData?.errors?.title) {
titleRef.current?.focus()
} else if (actionData?.errors?.body) {
bodyRef.current?.focus()
}
}, [actionData])
return (
<Form
method="post"
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
width: '100%'
}}
>
<div>
<label className="flex w-full flex-col gap-1">
<span>Title: </span>
<input
ref={titleRef}
name="title"
className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose"
aria-invalid={actionData?.errors?.title ? true : undefined}
aria-errormessage={actionData?.errors?.title ? 'title-error' : undefined}
/>
</label>
{actionData?.errors?.title && (
<div className="pt-1 text-red-700" id="title-error">
{actionData.errors.title}
</div>
)}
</div>
<div>
<label className="flex w-full flex-col gap-1">
<span>Body: </span>
<textarea
ref={bodyRef}
name="body"
rows={8}
className="w-full flex-1 rounded-md border-2 border-blue-500 py-2 px-3 text-lg leading-6"
aria-invalid={actionData?.errors?.body ? true : undefined}
aria-errormessage={actionData?.errors?.body ? 'body-error' : undefined}
/>
</label>
{actionData?.errors?.body && (
<div className="pt-1 text-red-700" id="body-error">
{actionData.errors.body}
</div>
)}
</div>
<div className="text-right">
<button type="submit" className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400">
Save
</button>
</div>
</Form>
)
}