2023-02-14 10:27:27 +01:00
|
|
|
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node';
|
|
|
|
|
import { json, redirect } from '@remix-run/node';
|
|
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
useActionData,
|
|
|
|
|
useCatch,
|
|
|
|
|
useLoaderData,
|
|
|
|
|
useNavigate
|
|
|
|
|
} from '@remix-run/react';
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Alert,
|
|
|
|
|
Drawer,
|
|
|
|
|
TextInput,
|
|
|
|
|
Text,
|
|
|
|
|
useMantineTheme,
|
|
|
|
|
Group,
|
|
|
|
|
Button,
|
|
|
|
|
Textarea,
|
|
|
|
|
Stack,
|
|
|
|
|
Select,
|
|
|
|
|
ColorSwatch,
|
|
|
|
|
ActionIcon
|
|
|
|
|
} from '@mantine/core';
|
|
|
|
|
import {
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
Delete,
|
|
|
|
|
Play,
|
|
|
|
|
Save,
|
|
|
|
|
Square,
|
|
|
|
|
Trash
|
|
|
|
|
} from 'react-feather';
|
|
|
|
|
import invariant from 'tiny-invariant';
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
import {
|
|
|
|
|
deleteTimeEntry,
|
|
|
|
|
getTimeEntry,
|
|
|
|
|
updateTimeEntry
|
|
|
|
|
} from '~/models/timeEntry.server';
|
|
|
|
|
import { requireUserId } from '~/session.server';
|
|
|
|
|
import { getProjects } from '~/models/project.server';
|
|
|
|
|
import { DatePicker, TimeInput } from '@mantine/dates';
|
|
|
|
|
|
|
|
|
|
export const meta: MetaFunction = () => {
|
|
|
|
|
return {
|
|
|
|
|
title: 'Edit Time Entry | WorkTimer',
|
|
|
|
|
description: 'Edit a time entry. You must be logged in to do this.'
|
|
|
|
|
};
|
|
|
|
|
};
|
2023-02-11 03:14:14 +01:00
|
|
|
|
|
|
|
|
export async function loader({ request, params }: LoaderArgs) {
|
2023-02-14 10:27:27 +01:00
|
|
|
const userId = await requireUserId(request);
|
|
|
|
|
invariant(params.timeEntryId, 'timeEntryId not found');
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
const timeEntry = await getTimeEntry({ userId, id: params.timeEntryId });
|
|
|
|
|
if (!timeEntry) {
|
|
|
|
|
throw new Response('Not Found', { status: 404 });
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
2023-02-14 10:27:27 +01:00
|
|
|
|
|
|
|
|
const projects = await getProjects({ userId });
|
|
|
|
|
|
|
|
|
|
return json({ timeEntry, projects });
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function action({ request, params }: ActionArgs) {
|
2023-02-14 10:27:27 +01:00
|
|
|
const userId = await requireUserId(request);
|
|
|
|
|
invariant(params.timeEntryId, 'timeEntryId not found');
|
|
|
|
|
|
|
|
|
|
const timeEntry = await getTimeEntry({ userId, id: params.timeEntryId });
|
|
|
|
|
if (!timeEntry) {
|
|
|
|
|
throw new Response('Not Found', { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request.method === 'DELETE') {
|
|
|
|
|
await deleteTimeEntry({ userId, id: params.timeEntryId });
|
|
|
|
|
} else if (request.method === 'PATCH') {
|
|
|
|
|
const formData = await request.formData();
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
const description = (formData.get('description') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
|
|
|
|
const projectId = (formData.get('projectId') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
|
|
|
|
let startTime = (formData.get('startTime') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
|
|
|
|
let endTime = (formData.get('endTime') || undefined) as string | undefined;
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
if (
|
|
|
|
|
startTime &&
|
|
|
|
|
typeof startTime === 'string' &&
|
|
|
|
|
Number.isNaN(Date.parse(startTime))
|
|
|
|
|
) {
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
projectId: null,
|
|
|
|
|
description: null,
|
|
|
|
|
startTime: 'startTime is invalid',
|
|
|
|
|
endTime: null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ status: 422 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
endTime &&
|
|
|
|
|
typeof endTime === 'string' &&
|
|
|
|
|
Number.isNaN(Date.parse(endTime))
|
|
|
|
|
) {
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
projectId: null,
|
|
|
|
|
description: null,
|
|
|
|
|
startTime: null,
|
|
|
|
|
endTime: 'endTime is invalid'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ status: 422 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
startTime &&
|
|
|
|
|
endTime &&
|
|
|
|
|
typeof startTime === 'string' &&
|
|
|
|
|
typeof endTime === 'string' &&
|
|
|
|
|
new Date(startTime) > new Date(endTime)
|
|
|
|
|
) {
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
projectId: null,
|
|
|
|
|
description: null,
|
|
|
|
|
startTime: 'startTime must be before endTime',
|
|
|
|
|
endTime: 'startTime must be before endTime'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ status: 422 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await updateTimeEntry({
|
|
|
|
|
timeEntryId: params.timeEntryId,
|
|
|
|
|
description,
|
|
|
|
|
projectId,
|
|
|
|
|
startTime: startTime ? new Date(startTime) : undefined,
|
2023-02-19 14:07:24 +01:00
|
|
|
endTime: endTime ? new Date(endTime) : undefined,
|
|
|
|
|
duration:
|
|
|
|
|
endTime && startTime
|
|
|
|
|
? new Date(endTime).getTime() - new Date(startTime).getTime()
|
|
|
|
|
: undefined
|
2023-02-14 10:27:27 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return redirect('/time-entries');
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
color: string;
|
|
|
|
|
}
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
const SelectItem = React.forwardRef<HTMLDivElement, ItemProps>(
|
|
|
|
|
({ label, color, id, ...others }: ItemProps, ref) => (
|
|
|
|
|
<div key={id} ref={ref} {...others}>
|
|
|
|
|
<Group noWrap>
|
|
|
|
|
<ColorSwatch color={color} />
|
|
|
|
|
<Text size="sm">{label}</Text>
|
|
|
|
|
</Group>
|
2023-02-11 03:14:14 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
2023-02-14 10:27:27 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const LayoutWrapper = ({ children }: React.PropsWithChildren<{}>) => {
|
|
|
|
|
const theme = useMantineTheme();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Drawer
|
|
|
|
|
opened
|
|
|
|
|
position="right"
|
|
|
|
|
title="Edit Time Entry"
|
|
|
|
|
padding="xl"
|
|
|
|
|
size="xl"
|
|
|
|
|
overlayColor={
|
|
|
|
|
theme.colorScheme === 'dark'
|
|
|
|
|
? theme.colors.dark[9]
|
|
|
|
|
: theme.colors.gray[2]
|
|
|
|
|
}
|
|
|
|
|
overlayOpacity={0.55}
|
|
|
|
|
overlayBlur={3}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
navigate('/time-entries');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Drawer>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function TimeEntryDetailsPage() {
|
|
|
|
|
const actionData = useActionData<typeof action>();
|
|
|
|
|
const data = useLoaderData<typeof loader>();
|
|
|
|
|
const theme = useMantineTheme();
|
|
|
|
|
|
|
|
|
|
const descriptionRef = React.useRef<HTMLTextAreaElement>(null);
|
|
|
|
|
const startDateRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
const endDateRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
const projectRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
const [start, setStart] = React.useState<Date>(
|
|
|
|
|
new Date(data.timeEntry.startTime || Date.now())
|
|
|
|
|
);
|
|
|
|
|
const [end, setEnd] = React.useState<Date | undefined>(
|
|
|
|
|
data.timeEntry.endTime ? new Date(data.timeEntry.endTime) : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (actionData?.errors?.description) {
|
|
|
|
|
descriptionRef.current?.focus();
|
|
|
|
|
} else if (actionData?.errors?.startTime) {
|
|
|
|
|
startDateRef.current?.focus();
|
|
|
|
|
} else if (actionData?.errors?.endTime) {
|
|
|
|
|
endDateRef.current?.focus();
|
|
|
|
|
} else if (actionData?.errors?.projectId) {
|
|
|
|
|
projectRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [actionData]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<LayoutWrapper>
|
|
|
|
|
<Form
|
2023-02-18 20:38:59 +01:00
|
|
|
method="patch"
|
2023-02-14 10:27:27 +01:00
|
|
|
noValidate
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
gap: 8,
|
|
|
|
|
width: '100%'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Textarea
|
|
|
|
|
mb={12}
|
|
|
|
|
withAsterisk
|
|
|
|
|
label="Description"
|
|
|
|
|
placeholder="What are you working on?"
|
|
|
|
|
id="new-description"
|
|
|
|
|
ref={descriptionRef}
|
|
|
|
|
defaultValue={data.timeEntry.description}
|
|
|
|
|
required
|
|
|
|
|
autoFocus={true}
|
|
|
|
|
name="description"
|
|
|
|
|
aria-invalid={actionData?.errors?.description ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.description}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.description }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Select
|
|
|
|
|
id="new-project"
|
|
|
|
|
ref={projectRef}
|
|
|
|
|
name="projectId"
|
|
|
|
|
mb={12}
|
|
|
|
|
label="Project"
|
|
|
|
|
defaultValue={data.timeEntry.projectId}
|
|
|
|
|
placeholder="Select project"
|
|
|
|
|
searchable
|
|
|
|
|
nothingFound="No options"
|
|
|
|
|
required
|
|
|
|
|
withAsterisk
|
|
|
|
|
maxDropdownHeight={400}
|
|
|
|
|
data={data.projects.projects.map((project) => ({
|
|
|
|
|
label: project.name,
|
|
|
|
|
value: project.id,
|
|
|
|
|
color: project.color
|
|
|
|
|
}))}
|
|
|
|
|
itemComponent={SelectItem}
|
|
|
|
|
filter={(value, item) =>
|
|
|
|
|
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
|
|
|
|
|
item.value.toLowerCase().includes(value.toLowerCase().trim())
|
|
|
|
|
}
|
|
|
|
|
aria-invalid={actionData?.errors?.projectId ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.projectId}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.projectId }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Stack>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="new-startTime"
|
|
|
|
|
style={{
|
|
|
|
|
margin: '1rem 0 -0.5rem 0'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Start
|
|
|
|
|
</label>
|
2023-02-18 20:42:59 +01:00
|
|
|
<Group
|
|
|
|
|
align="flex-end"
|
|
|
|
|
sx={{
|
|
|
|
|
flexWrap: 'nowrap'
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-02-14 10:27:27 +01:00
|
|
|
<DatePicker
|
|
|
|
|
id="new-startTime-date"
|
|
|
|
|
ref={startDateRef}
|
|
|
|
|
name="startTime-date"
|
|
|
|
|
allowFreeInput
|
|
|
|
|
withAsterisk
|
|
|
|
|
clearable={false}
|
|
|
|
|
inputFormat="DD/MM/YYYY"
|
|
|
|
|
labelFormat="MM/YYYY"
|
|
|
|
|
aria-labelledby="new-startTime-label"
|
|
|
|
|
locale="it"
|
|
|
|
|
placeholder="Start date"
|
|
|
|
|
label="Start date"
|
|
|
|
|
aria-invalid={actionData?.errors?.startTime ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.startTime}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.startTime }}
|
|
|
|
|
value={start}
|
|
|
|
|
onChange={(date) => {
|
|
|
|
|
if (!date) return;
|
|
|
|
|
let newDate = new Date(start);
|
|
|
|
|
newDate.setFullYear(date.getFullYear());
|
|
|
|
|
newDate.setMonth(date.getMonth());
|
|
|
|
|
newDate.setDate(date.getDate());
|
|
|
|
|
setStart(newDate);
|
|
|
|
|
}}
|
2023-02-20 15:13:21 +01:00
|
|
|
firstDayOfWeek="monday"
|
|
|
|
|
renderDay={(date) => {
|
|
|
|
|
const day = date.getDate();
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
|
|
const isToday =
|
|
|
|
|
date.getDate() === today.getDate() &&
|
|
|
|
|
date.getMonth() === today.getMonth() &&
|
|
|
|
|
date.getFullYear() === today.getFullYear();
|
|
|
|
|
|
|
|
|
|
return isToday ? (
|
|
|
|
|
<Text component="div" weight="bold" underline>
|
|
|
|
|
{day}
|
|
|
|
|
</Text>
|
|
|
|
|
) : (
|
|
|
|
|
<Text component="div">{day}</Text>
|
|
|
|
|
);
|
|
|
|
|
}}
|
2023-02-14 10:27:27 +01:00
|
|
|
/>
|
|
|
|
|
<TimeInput
|
|
|
|
|
id="new-startTime-time"
|
|
|
|
|
ref={startDateRef}
|
|
|
|
|
name="startTime-time"
|
|
|
|
|
withAsterisk
|
|
|
|
|
withSeconds
|
|
|
|
|
clearable={false}
|
|
|
|
|
aria-labelledby="new-startTime-label"
|
|
|
|
|
label="Start time"
|
|
|
|
|
value={start}
|
|
|
|
|
onChange={(date) => {
|
|
|
|
|
let newDate = new Date(start);
|
|
|
|
|
newDate.setHours(date.getHours());
|
|
|
|
|
newDate.setMinutes(date.getMinutes());
|
|
|
|
|
setStart(newDate);
|
|
|
|
|
}}
|
|
|
|
|
aria-invalid={actionData?.errors?.startTime ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.startTime}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.startTime }}
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
|
|
|
|
<input type="hidden" name="startTime" value={start.toISOString()} />
|
|
|
|
|
</Stack>
|
|
|
|
|
|
|
|
|
|
<Stack>
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="new-endTime"
|
|
|
|
|
style={{
|
|
|
|
|
margin: '1rem 0 -0.5rem 0'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
End
|
|
|
|
|
</label>
|
2023-02-18 20:42:59 +01:00
|
|
|
<Group
|
|
|
|
|
align="flex-end"
|
|
|
|
|
sx={{
|
|
|
|
|
flexWrap: 'nowrap'
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-02-14 10:27:27 +01:00
|
|
|
<DatePicker
|
|
|
|
|
id="new-endTime-date"
|
|
|
|
|
ref={endDateRef}
|
|
|
|
|
name="endTime-date"
|
|
|
|
|
allowFreeInput
|
|
|
|
|
clearable={true}
|
|
|
|
|
inputFormat="DD/MM/YYYY"
|
|
|
|
|
labelFormat="MM/YYYY"
|
|
|
|
|
aria-labelledby="new-endTime-label"
|
|
|
|
|
locale="it"
|
|
|
|
|
placeholder="End date"
|
|
|
|
|
label="End date"
|
|
|
|
|
aria-invalid={actionData?.errors?.endTime ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.endTime}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.endTime }}
|
|
|
|
|
value={end}
|
|
|
|
|
onChange={(date) => {
|
|
|
|
|
if (!date) return;
|
|
|
|
|
let newDate = new Date(end || start);
|
|
|
|
|
newDate.setFullYear(date.getFullYear());
|
|
|
|
|
newDate.setMonth(date.getMonth());
|
|
|
|
|
newDate.setDate(date.getDate());
|
|
|
|
|
setEnd(newDate);
|
|
|
|
|
}}
|
2023-02-20 15:13:21 +01:00
|
|
|
firstDayOfWeek="monday"
|
|
|
|
|
renderDay={(date) => {
|
|
|
|
|
const day = date.getDate();
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
|
|
|
|
const isToday =
|
|
|
|
|
date.getDate() === today.getDate() &&
|
|
|
|
|
date.getMonth() === today.getMonth() &&
|
|
|
|
|
date.getFullYear() === today.getFullYear();
|
|
|
|
|
|
|
|
|
|
return isToday ? (
|
|
|
|
|
<Text component="div" weight="bold" underline>
|
|
|
|
|
{day}
|
|
|
|
|
</Text>
|
|
|
|
|
) : (
|
|
|
|
|
<Text component="div">{day}</Text>
|
|
|
|
|
);
|
|
|
|
|
}}
|
2023-02-14 10:27:27 +01:00
|
|
|
/>
|
|
|
|
|
<TimeInput
|
|
|
|
|
id="new-endTime-time"
|
|
|
|
|
ref={endDateRef}
|
|
|
|
|
name="endTime-time"
|
|
|
|
|
withAsterisk
|
|
|
|
|
withSeconds
|
|
|
|
|
clearable={false}
|
|
|
|
|
aria-labelledby="new-endTime-label"
|
2023-02-18 20:42:59 +01:00
|
|
|
label="End time"
|
2023-02-14 10:27:27 +01:00
|
|
|
value={end}
|
|
|
|
|
onChange={(date) => {
|
|
|
|
|
let newDate = new Date(end || start);
|
|
|
|
|
newDate.setHours(date.getHours());
|
|
|
|
|
newDate.setMinutes(date.getMinutes());
|
|
|
|
|
setEnd(newDate);
|
|
|
|
|
}}
|
|
|
|
|
aria-invalid={actionData?.errors?.endTime ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.endTime}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.endTime }}
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
|
|
|
|
{end && (
|
|
|
|
|
<input type="hidden" name="endTime" value={end.toISOString()} />
|
|
|
|
|
)}
|
|
|
|
|
</Stack>
|
|
|
|
|
|
|
|
|
|
<Group position="left" mt="lg">
|
|
|
|
|
<Button type="submit" leftIcon={<Save />} radius={theme.radius.md}>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
<section style={{ marginTop: '1rem', marginBottom: '1rem' }}>
|
|
|
|
|
<Form method="delete">
|
|
|
|
|
<input
|
|
|
|
|
type="hidden"
|
|
|
|
|
name="endTime"
|
|
|
|
|
value={new Date().toISOString()}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="outline"
|
|
|
|
|
color="red"
|
|
|
|
|
leftIcon={<Trash />}
|
|
|
|
|
radius={theme.radius.md}
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</Button>
|
|
|
|
|
</Form>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{!end && (
|
|
|
|
|
<section style={{ marginTop: '1rem', marginBottom: '1rem' }}>
|
|
|
|
|
<Form method="patch">
|
|
|
|
|
<input
|
|
|
|
|
type="hidden"
|
|
|
|
|
name="endTime"
|
|
|
|
|
value={new Date().toISOString()}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="subtle"
|
|
|
|
|
radius={theme.radius.md}
|
|
|
|
|
leftIcon={
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: theme.colors.red[7],
|
|
|
|
|
color: 'white',
|
|
|
|
|
width: '1.75rem',
|
|
|
|
|
height: '1.75rem',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
borderRadius: '50%'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Square size={12} fill="currentColor" />
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
Stop running time entry
|
|
|
|
|
</Button>
|
|
|
|
|
</Form>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
</LayoutWrapper>
|
|
|
|
|
);
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ErrorBoundary({ error }: { error: Error }) {
|
2023-02-14 10:27:27 +01:00
|
|
|
console.error(error);
|
2023-02-11 03:14:14 +01:00
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
return (
|
|
|
|
|
<LayoutWrapper>
|
|
|
|
|
<Alert icon={<AlertTriangle size={14} />} title="Error" color="red">
|
|
|
|
|
An unexpected error occurred: {error.message}
|
|
|
|
|
</Alert>
|
|
|
|
|
</LayoutWrapper>
|
|
|
|
|
);
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function CatchBoundary() {
|
2023-02-14 10:27:27 +01:00
|
|
|
const caught = useCatch();
|
2023-02-11 03:14:14 +01:00
|
|
|
|
|
|
|
|
if (caught.status === 404) {
|
2023-02-14 10:27:27 +01:00
|
|
|
return (
|
|
|
|
|
<LayoutWrapper>
|
|
|
|
|
<Alert icon={<AlertTriangle size={14} />} title="Error" color="red">
|
|
|
|
|
Not found
|
|
|
|
|
</Alert>
|
|
|
|
|
</LayoutWrapper>
|
|
|
|
|
);
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|
|
|
|
|
|
2023-02-14 10:27:27 +01:00
|
|
|
throw new Error(`Unexpected caught response with status: ${caught.status}`);
|
2023-02-11 03:14:14 +01:00
|
|
|
}
|