124 lines
4 KiB
TypeScript
124 lines
4 KiB
TypeScript
|
|
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>
|
||
|
|
)
|
||
|
|
}
|