205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
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";
|
|
|
|
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 },
|
|
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 (
|
|
<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>
|
|
);
|
|
}
|