From e0843fc89a558a02be0c35fc8fdb65e2c8ccfb68 Mon Sep 17 00:00:00 2001 From: nzambello Date: Tue, 14 Feb 2023 10:27:43 +0100 Subject: [PATCH] feat: add projects management --- app/routes/projects.tsx | 240 +++++++++++++++++++++++++ app/routes/projects/$projectId.tsx | 274 +++++++++++++++++++++++++++++ app/routes/projects/new.tsx | 248 ++++++++++++++++++++++++++ 3 files changed, 762 insertions(+) create mode 100644 app/routes/projects.tsx create mode 100644 app/routes/projects/$projectId.tsx create mode 100644 app/routes/projects/new.tsx diff --git a/app/routes/projects.tsx b/app/routes/projects.tsx new file mode 100644 index 0000000..e4ebb89 --- /dev/null +++ b/app/routes/projects.tsx @@ -0,0 +1,240 @@ +import { useMemo } from 'react'; +import { + Button, + Paper, + Text, + Menu, + ActionIcon, + Pagination, + NativeSelect, + Group, + useMantineTheme, + Alert, + ColorSwatch +} from '@mantine/core'; +import { json, LoaderArgs, MetaFunction, redirect } from '@remix-run/node'; +import { + Form, + Link, + Outlet, + useCatch, + useLoaderData, + useSearchParams +} from '@remix-run/react'; +import { AlertTriangle, Edit3, Plus, Settings, Trash } from 'react-feather'; +import { requireUserId } from '~/session.server'; +import { getProjects } from '~/models/project.server'; + +export const meta: MetaFunction = () => { + return { + title: 'Projects | WorkTimer', + description: 'Manage your projects. You must be logged in to do this.' + }; +}; + +export async function loader({ request }: LoaderArgs) { + const userId = await requireUserId(request); + if (!userId) return redirect('/login'); + + const url = new URL(request.url); + const page = url.searchParams.get('page') + ? parseInt(url.searchParams.get('page')!, 10) + : 1; + const size = url.searchParams.get('size') + ? parseInt(url.searchParams.get('size')!, 10) + : 25; + const orderBy = url.searchParams.get('orderBy') || 'createdAt'; + const order = url.searchParams.get('order') || 'desc'; + + return json({ + ...(await getProjects({ + page, + size, + userId, + orderBy, + order: order === 'asc' ? 'asc' : 'desc' + })) + }); +} + +export default function Projects() { + const data = useLoaderData(); + const [searchParams, setSearchParams] = useSearchParams(); + const theme = useMantineTheme(); + + const pageSize = useMemo(() => { + return parseInt(searchParams.get('size') || '25', 10); + }, [searchParams]); + const page = useMemo(() => { + return parseInt(searchParams.get('page') || '1', 10); + }, [searchParams]); + + return ( +
+ + +
+ { + setSearchParams({ + page: page.toString(), + size: event.currentTarget.value + }); + }} + /> + {data.total / pageSize > 1 && ( + { + setSearchParams({ + page: page.toString(), + size: pageSize.toString() + }); + }} + /> + )} +
+
+ + + {data.total} entries + + + +
+ {data.projects.map((project) => ( + +
+ +
+ {project.name} + + {project.description} + +
+ + + + + + + + + Edit project + } + > + Edit + +
+ } + > + Delete + +
+
+
+
+
+ ))} +
+ +
+ ); +} + +export function ErrorBoundary({ error }: { error: Error }) { + console.error(error); + + return ( + } title="Error" color="red"> + An unexpected error occurred: {error.message} + + ); +} + +export function CatchBoundary() { + const caught = useCatch(); + + if (caught.status === 404) { + return ( + } title="Error" color="red"> + Not found + + ); + } + + throw new Error(`Unexpected caught response with status: ${caught.status}`); +} diff --git a/app/routes/projects/$projectId.tsx b/app/routes/projects/$projectId.tsx new file mode 100644 index 0000000..06751fe --- /dev/null +++ b/app/routes/projects/$projectId.tsx @@ -0,0 +1,274 @@ +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, + ColorInput, + ActionIcon, + Input, + ColorPicker +} from '@mantine/core'; +import { + AlertTriangle, + Delete, + Play, + RefreshCcw, + Save, + Square, + Trash +} from 'react-feather'; +import invariant from 'tiny-invariant'; + +import { + deleteTimeEntry, + getTimeEntry, + updateTimeEntry +} from '~/models/timeEntry.server'; +import { requireUserId } from '~/session.server'; +import { + deleteProject, + getProject, + getProjects, + Project, + updateProject +} from '~/models/project.server'; +import { DatePicker, TimeInput } from '@mantine/dates'; + +export const meta: MetaFunction = () => { + return { + title: 'Edit Project | WorkTimer', + description: 'Edit a project. You must be logged in to do this.' + }; +}; + +export async function loader({ request, params }: LoaderArgs) { + const userId = await requireUserId(request); + invariant(params.projectId, 'projectId not found'); + + const project = await getProject({ userId, id: params.projectId }); + if (!project) { + throw new Response('Not Found', { status: 404 }); + } + + return json({ project }); +} + +export async function action({ request, params }: ActionArgs) { + const userId = await requireUserId(request); + invariant(params.projectId, 'projectId not found'); + + const project = await getProject({ userId, id: params.projectId }); + if (!project) { + throw new Response('Not Found', { status: 404 }); + } + + if (request.method === 'DELETE') { + await deleteProject({ userId, id: params.projectId }); + } else if (request.method === 'PATCH') { + const formData = await request.formData(); + + const name = (formData.get('name') || undefined) as string | undefined; + const description = (formData.get('description') || undefined) as + | string + | undefined; + let color = (formData.get('color') || undefined) as string | undefined; + + await updateProject({ + projectId: params.projectId, + name, + description, + color + }); + } + + return redirect('/projects'); +} + +const LayoutWrapper = ({ children }: React.PropsWithChildren<{}>) => { + const theme = useMantineTheme(); + const navigate = useNavigate(); + + return ( + { + navigate('/projects'); + }} + > + {children} + + ); +}; + +const randomColor = () => + `#${Math.floor(Math.random() * 16777215).toString(16)}`; + +export default function ProjectDetailsPage() { + const actionData = useActionData(); + const data = useLoaderData(); + const theme = useMantineTheme(); + + const nameRef = React.useRef(null); + const descriptionRef = React.useRef(null); + const colorRef = React.useRef(null); + + const [color, setColor] = React.useState( + data.project.color || randomColor() + ); + + return ( + +
+ + +