explit/app/routes/expenses/$expenseId.tsx

205 lines
6.4 KiB
TypeScript
Raw Normal View History

import type { ActionFunction, LoaderFunction, MetaFunction } from "remix";
import {
Link,
useLoaderData,
useParams,
useCatch,
redirect,
Form,
} from "remix";
import type { Expense, User } from "@prisma/client";
import { db } from "~/utils/db.server";
import { requireUserId, getUserId } from "~/utils/session.server";
2022-02-14 20:48:34 +01:00
type LoaderData = { expense: Expense & { user: User }; isOwner: boolean };
export const meta: MetaFunction = ({
data,
}: {
data: LoaderData | undefined;
}) => {
if (!data) {
return {
title: "No expense",
description: "No expense found",
};
}
return {
title: `Expense: ${data.expense.description} | Explit`,
description: `Details of expense: ${data.expense.description}`,
};
};
export const loader: LoaderFunction = async ({ request, params }) => {
const userId = await getUserId(request);
if (!userId) {
redirect("/login");
}
const expense = await db.expense.findUnique({
where: { id: params.expenseId },
2022-02-14 20:48:34 +01:00
include: {
user: true,
},
});
if (!expense) {
throw new Response("What an expense! Not found.", {
status: 404,
});
}
const data: LoaderData = {
expense,
isOwner: userId === expense.userId,
};
return data;
};
export const action: ActionFunction = async ({ request, params }) => {
const form = await request.formData();
if (form.get("_method") === "delete") {
const userId = await requireUserId(request);
const expense = await db.expense.findUnique({
where: { id: params.expenseId },
});
if (!expense) {
throw new Response("Can't delete what does not exist", { status: 404 });
}
if (expense.userId !== userId) {
throw new Response("Pssh, nice try. That's not your expense", {
status: 401,
});
}
await db.expense.delete({ where: { id: params.expenseId } });
return redirect("/expenses");
}
};
export default function ExpenseRoute() {
const data = useLoaderData<LoaderData>();
return (
2022-02-14 20:48:34 +01:00
<div className="card shadow-lg bg-base-100 my-4">
<div className="flex-col items-center card-body p-4">
<div className="flex flex-row w-full items-center">
<Link to="/expenses" className="btn btn-ghost mr-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block w-6 h-6 mr-2 stroke-current rotate-180"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
></path>
</svg>
Back
</Link>
<h2 className="card-title mb-0">Expense details</h2>
</div>
<dl className="w-full py-6 px-3 my-4 rounded-box bg-base-200">
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-base-400">Description</dt>
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
{data.expense.description}
</dd>
</div>
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-base-400">Amount</dt>
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
{data.expense.amount}
</dd>
</div>
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 items-center">
<dt className="text-sm font-medium text-base-400">User</dt>
<dd className="mt-2 text-sm text-base-900 sm:mt-0 sm:col-span-2 flex items-center">
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl mr-4">
{data.expense.user.icon ?? data.expense.user.username[0]}
</div>
{data.expense.user.username}
</dd>
</div>
<div className="bg-base-200 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-base-400">Date</dt>
<dd className="mt-1 text-sm text-base-900 sm:mt-0 sm:col-span-2">
{new Intl.DateTimeFormat("it", {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(data.expense.createdAt))}
</dd>
</div>
</dl>
{data.isOwner && (
<>
<div className="flex justify-center align-center mt-6">
<label
htmlFor="delete-expense-modal"
className="btn btn-error modal-button"
>
Delete expense
</label>
</div>
<input
type="checkbox"
id="delete-expense-modal"
className="modal-toggle"
/>
<div className="modal">
<div className="modal-box absolute left-[10%] right-[10%] top-[40%] w-[80%] rounded-lg">
<p>
Do you really want to delete your expense? Its data will be
permanently deleted.
</p>
<div className="modal-action">
<Form method="post">
<input type="hidden" name="_method" value="delete" />
<button type="submit" className="btn btn-error">
Delete
</button>
</Form>
<label htmlFor="delete-expense-modal" className="btn">
Cancel
</label>
</div>
</div>
</div>
</>
)}
</div>
</div>
);
}
export function CatchBoundary() {
const caught = useCatch();
const params = useParams();
switch (caught.status) {
case 404: {
return (
<div className="error-container">
Huh? What the heck is {params.expenseId}?
</div>
);
}
case 401: {
return (
<div className="error-container">
Sorry, but {params.expenseId} is not your sheet.
</div>
);
}
default: {
throw new Error(`Unhandled error: ${caught.status}`);
}
}
}
export function ErrorBoundary() {
const { expenseId } = useParams();
return (
<div className="error-container">{`There was an error loading expense by the id ${expenseId}. Sorry.`}</div>
);
}