feat: add expense detail ui

This commit is contained in:
Nicola Zambello 2022-02-14 20:48:34 +01:00
parent 0c354b069a
commit 8afc80539b
3 changed files with 214 additions and 122 deletions

View file

@ -42,44 +42,46 @@ const Header = ({ user, route }: Props) => {
</Link> </Link>
</div> </div>
<div className="flex-none hidden lg:block"> <div className="flex-none hidden lg:block">
<ul className="menu horizontal"> {user && (
<li> <ul className="menu horizontal">
<Link to="/expenses" className="btn btn-ghost rounded-btn"> <li>
<MoneyAdd className="lg:mr-2" /> <Link to="/expenses" className="btn btn-ghost rounded-btn">
<span <MoneyAdd className="lg:mr-2" />
className={`hidden lg:inline-block ${ <span
route === "/expenses" ? "underline" : "" className={`hidden lg:inline-block ${
}`} route === "/expenses" ? "underline" : ""
> }`}
Expenses >
</span> Expenses
</Link> </span>
</li> </Link>
<li> </li>
<Link to="/team" className="btn btn-ghost rounded-btn"> <li>
<People className="lg:mr-2" /> <Link to="/team" className="btn btn-ghost rounded-btn">
<span <People className="lg:mr-2" />
className={`hidden lg:inline-block ${ <span
route === "/team" ? "underline" : "" className={`hidden lg:inline-block ${
}`} route === "/team" ? "underline" : ""
> }`}
Your team >
</span> Your team
</Link> </span>
</li> </Link>
<li> </li>
<Link to="/statistics" className="btn btn-ghost rounded-btn"> <li>
<Percentage className="lg:mr-2" /> <Link to="/statistics" className="btn btn-ghost rounded-btn">
<span <Percentage className="lg:mr-2" />
className={`hidden lg:inline-block ${ <span
route === "/statistics" ? "underline" : "" className={`hidden lg:inline-block ${
}`} route === "/statistics" ? "underline" : ""
> }`}
Statistics >
</span> Statistics
</Link> </span>
</li> </Link>
</ul> </li>
</ul>
)}
</div> </div>
<div className="flex-none"> <div className="flex-none">
{user ? ( {user ? (
@ -154,42 +156,57 @@ const Header = ({ user, route }: Props) => {
<span className="text-lg font-bold">Explit</span> <span className="text-lg font-bold">Explit</span>
</Link> </Link>
</li> </li>
<li className="items-start"> {user ? (
<Link to="/expenses" className="btn btn-ghost rounded-btn"> <>
<MoneyAdd className="mr-2" /> <li className="items-start">
<span <Link to="/expenses" className="btn btn-ghost rounded-btn">
className={`inline-block ${ <MoneyAdd className="mr-2" />
route === "/expenses" ? "underline" : "" <span
}`} className={`inline-block ${
route === "/expenses" ? "underline" : ""
}`}
>
Expenses
</span>
</Link>
</li>
<li className="items-start">
<Link to="/team" className="btn btn-ghost rounded-btn">
<People className="mr-2" />
<span
className={`inline-block ${
route === "/team" ? "underline" : ""
}`}
>
Your team
</span>
</Link>
</li>
<li className="items-start">
<Link to="/statistics" className="btn btn-ghost rounded-btn">
<Percentage className="mr-2" />
<span
className={`inline-block ${
route === "/statistics" ? "underline" : ""
}`}
>
Statistics
</span>
</Link>
</li>
</>
) : (
<li className="items-start">
<Link
to="/login"
className="btn btn-ghost rounded-btn"
title="Login"
> >
Expenses <LoginSVG />
</span> <span className="inline-block ml-1">Login</span>
</Link> </Link>
</li> </li>
<li className="items-start"> )}
<Link to="/team" className="btn btn-ghost rounded-btn">
<People className="mr-2" />
<span
className={`inline-block ${
route === "/team" ? "underline" : ""
}`}
>
Your team
</span>
</Link>
</li>
<li className="items-start">
<Link to="/statistics" className="btn btn-ghost rounded-btn">
<Percentage className="mr-2" />
<span
className={`inline-block ${
route === "/statistics" ? "underline" : ""
}`}
>
Statistics
</span>
</Link>
</li>
</ul> </ul>
</div> </div>
</header> </header>

View file

@ -11,7 +11,7 @@ import type { Expense, User } from "@prisma/client";
import { db } from "~/utils/db.server"; import { db } from "~/utils/db.server";
import { requireUserId, getUserId } from "~/utils/session.server"; import { requireUserId, getUserId } from "~/utils/session.server";
type LoaderData = { expense: Expense; user: User; isOwner: boolean }; type LoaderData = { expense: Expense & { user: User }; isOwner: boolean };
export const meta: MetaFunction = ({ export const meta: MetaFunction = ({
data, data,
@ -38,23 +38,17 @@ export const loader: LoaderFunction = async ({ request, params }) => {
const expense = await db.expense.findUnique({ const expense = await db.expense.findUnique({
where: { id: params.expenseId }, where: { id: params.expenseId },
include: {
user: true,
},
}); });
if (!expense) { if (!expense) {
throw new Response("What an expense! Not found.", { throw new Response("What an expense! Not found.", {
status: 404, status: 404,
}); });
} }
const expenseUser = await db.user.findUnique({
where: { id: expense.userId },
});
if (!expenseUser) {
throw new Response("Oupsie! Not found.", {
status: 500,
});
}
const data: LoaderData = { const data: LoaderData = {
expense, expense,
user: expenseUser,
isOwner: userId === expense.userId, isOwner: userId === expense.userId,
}; };
return data; return data;
@ -84,18 +78,96 @@ export default function ExpenseRoute() {
const data = useLoaderData<LoaderData>(); const data = useLoaderData<LoaderData>();
return ( return (
<div> <div className="card shadow-lg bg-base-100 my-4">
<p>Description: {data.expense.description}</p> <div className="flex-col items-center card-body p-4">
<p>Amount: {data.expense.amount}</p> <div className="flex flex-row w-full items-center">
<p>User: {data.user.username}</p> <Link to="/expenses" className="btn btn-ghost mr-4">
{data.isOwner && ( <svg
<form method="post"> xmlns="http://www.w3.org/2000/svg"
<input type="hidden" name="_method" value="delete" /> fill="none"
<button type="submit" className="button"> viewBox="0 0 24 24"
Delete className="inline-block w-6 h-6 mr-2 stroke-current rotate-180"
</button> >
</form> <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> </div>
); );
} }

View file

@ -78,7 +78,7 @@ export const loader: LoaderFunction = async ({ request }) => {
return data; return data;
}; };
export default function JokesIndexRoute() { export default function ExpensesIndexRoute() {
const data = useLoaderData<LoaderData>(); const data = useLoaderData<LoaderData>();
return ( return (
@ -88,28 +88,33 @@ export default function JokesIndexRoute() {
<h2 className="card-title">Last expenses</h2> <h2 className="card-title">Last expenses</h2>
<ul className="list-none shadow-inner w-full rounded-lg p-4 my-4 mx-0 max-h-48 overflow-x-scroll"> <ul className="list-none shadow-inner w-full rounded-lg p-4 my-4 mx-0 max-h-48 overflow-x-scroll">
{data.lastExpenses?.map((exp) => ( {data.lastExpenses?.map((exp) => (
<li className="flex w-full items-center mb-3" key={exp.id}> <li key={exp.id}>
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl"> <Link
{exp.user.icon ?? exp.user.username[0]} className="flex w-full items-center mb-3"
</div> to={`/expenses/${exp.id}`}
<div className="font-bold w-16 ml-2 text-right"> >
<span <div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
className={`${ {exp.user.icon ?? exp.user.username[0]}
exp.amount > 0 ? "text-error" : "text-success" </div>
}`} <div className="font-bold w-16 ml-2 text-right">
> <span
{-exp.amount} className={`${
</span> exp.amount > 0 ? "text-error" : "text-success"
</div> }`}
<div className="grow ml-3 flex flex-col justify-center items-start"> >
<span className="text-xs opacity-50"> {-exp.amount}
{new Intl.DateTimeFormat("it", { </span>
dateStyle: "short", </div>
timeStyle: "short", <div className="grow ml-3 flex flex-col justify-center items-start">
}).format(new Date(exp.createdAt))} <span className="text-xs opacity-50">
</span> {new Intl.DateTimeFormat("it", {
<span className="font-bold">{exp.description}</span> dateStyle: "short",
</div> timeStyle: "short",
}).format(new Date(exp.createdAt))}
</span>
<span className="font-bold">{exp.description}</span>
</div>
</Link>
</li> </li>
))} ))}
</ul> </ul>
@ -125,7 +130,7 @@ export default function JokesIndexRoute() {
{data.teamCounts?.map((user) => ( {data.teamCounts?.map((user) => (
<div className="stat col-span-1"> <div className="stat col-span-1">
<div className="stat-figure text-info"> <div className="stat-figure text-info">
<div className="rounded-full shrink-0 w-12 h-12 inline-flex justify-center items-center bg-white text-3xl"> <div className="rounded-full shrink-0 w-4 sm:w-10 h-4 sm:h-10 inline-flex justify-center items-center bg-white text-3xl">
{user.icon ?? user.username[0]} {user.icon ?? user.username[0]}
</div> </div>
</div> </div>
@ -140,9 +145,7 @@ export default function JokesIndexRoute() {
<div className="stat-desc text-info"> <div className="stat-desc text-info">
{user.dueAmount > 0 {user.dueAmount > 0
? `${user.id === data.user.id ? "You owe" : "Owes"} others` ? `${user.id === data.user.id ? "You owe" : "Owes"} others`
: `Others owe ${ : `Others owe ${user.id === data.user.id ? "you" : ""}`}
user.id === data.user.id ? "you" : "him/her"
}`}
</div> </div>
</div> </div>
))} ))}