feat: add expense detail ui
This commit is contained in:
parent
0c354b069a
commit
8afc80539b
|
|
@ -42,44 +42,46 @@ const Header = ({ user, route }: Props) => {
|
|||
</Link>
|
||||
</div>
|
||||
<div className="flex-none hidden lg:block">
|
||||
<ul className="menu horizontal">
|
||||
<li>
|
||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||
<MoneyAdd className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/expenses" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Expenses
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/team" className="btn btn-ghost rounded-btn">
|
||||
<People className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/team" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Your team
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
||||
<Percentage className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/statistics" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Statistics
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
{user && (
|
||||
<ul className="menu horizontal">
|
||||
<li>
|
||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||
<MoneyAdd className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/expenses" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Expenses
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/team" className="btn btn-ghost rounded-btn">
|
||||
<People className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/team" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Your team
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/statistics" className="btn btn-ghost rounded-btn">
|
||||
<Percentage className="lg:mr-2" />
|
||||
<span
|
||||
className={`hidden lg:inline-block ${
|
||||
route === "/statistics" ? "underline" : ""
|
||||
}`}
|
||||
>
|
||||
Statistics
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
{user ? (
|
||||
|
|
@ -154,42 +156,57 @@ const Header = ({ user, route }: Props) => {
|
|||
<span className="text-lg font-bold">Explit</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="items-start">
|
||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||
<MoneyAdd className="mr-2" />
|
||||
<span
|
||||
className={`inline-block ${
|
||||
route === "/expenses" ? "underline" : ""
|
||||
}`}
|
||||
{user ? (
|
||||
<>
|
||||
<li className="items-start">
|
||||
<Link to="/expenses" className="btn btn-ghost rounded-btn">
|
||||
<MoneyAdd className="mr-2" />
|
||||
<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
|
||||
</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>
|
||||
<LoginSVG />
|
||||
<span className="inline-block ml-1">Login</span>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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 };
|
||||
type LoaderData = { expense: Expense & { user: User }; isOwner: boolean };
|
||||
|
||||
export const meta: MetaFunction = ({
|
||||
data,
|
||||
|
|
@ -38,23 +38,17 @@ export const loader: LoaderFunction = async ({ request, params }) => {
|
|||
|
||||
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 expenseUser = await db.user.findUnique({
|
||||
where: { id: expense.userId },
|
||||
});
|
||||
if (!expenseUser) {
|
||||
throw new Response("Oupsie! Not found.", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
const data: LoaderData = {
|
||||
expense,
|
||||
user: expenseUser,
|
||||
isOwner: userId === expense.userId,
|
||||
};
|
||||
return data;
|
||||
|
|
@ -84,18 +78,96 @@ export default function ExpenseRoute() {
|
|||
const data = useLoaderData<LoaderData>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Description: {data.expense.description}</p>
|
||||
<p>Amount: {data.expense.amount}€</p>
|
||||
<p>User: {data.user.username}</p>
|
||||
{data.isOwner && (
|
||||
<form method="post">
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<button type="submit" className="button">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export const loader: LoaderFunction = async ({ request }) => {
|
|||
return data;
|
||||
};
|
||||
|
||||
export default function JokesIndexRoute() {
|
||||
export default function ExpensesIndexRoute() {
|
||||
const data = useLoaderData<LoaderData>();
|
||||
|
||||
return (
|
||||
|
|
@ -88,28 +88,33 @@ export default function JokesIndexRoute() {
|
|||
<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">
|
||||
{data.lastExpenses?.map((exp) => (
|
||||
<li className="flex w-full items-center mb-3" key={exp.id}>
|
||||
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
|
||||
{exp.user.icon ?? exp.user.username[0]}
|
||||
</div>
|
||||
<div className="font-bold w-16 ml-2 text-right">
|
||||
<span
|
||||
className={`${
|
||||
exp.amount > 0 ? "text-error" : "text-success"
|
||||
}`}
|
||||
>
|
||||
{-exp.amount} €
|
||||
</span>
|
||||
</div>
|
||||
<div className="grow ml-3 flex flex-col justify-center items-start">
|
||||
<span className="text-xs opacity-50">
|
||||
{new Intl.DateTimeFormat("it", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(exp.createdAt))}
|
||||
</span>
|
||||
<span className="font-bold">{exp.description}</span>
|
||||
</div>
|
||||
<li key={exp.id}>
|
||||
<Link
|
||||
className="flex w-full items-center mb-3"
|
||||
to={`/expenses/${exp.id}`}
|
||||
>
|
||||
<div className="rounded-full w-10 h-10 inline-flex justify-center items-center bg-white text-3xl">
|
||||
{exp.user.icon ?? exp.user.username[0]}
|
||||
</div>
|
||||
<div className="font-bold w-16 ml-2 text-right">
|
||||
<span
|
||||
className={`${
|
||||
exp.amount > 0 ? "text-error" : "text-success"
|
||||
}`}
|
||||
>
|
||||
{-exp.amount} €
|
||||
</span>
|
||||
</div>
|
||||
<div className="grow ml-3 flex flex-col justify-center items-start">
|
||||
<span className="text-xs opacity-50">
|
||||
{new Intl.DateTimeFormat("it", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(exp.createdAt))}
|
||||
</span>
|
||||
<span className="font-bold">{exp.description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -125,7 +130,7 @@ export default function JokesIndexRoute() {
|
|||
{data.teamCounts?.map((user) => (
|
||||
<div className="stat col-span-1">
|
||||
<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]}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,9 +145,7 @@ export default function JokesIndexRoute() {
|
|||
<div className="stat-desc text-info">
|
||||
{user.dueAmount > 0
|
||||
? `${user.id === data.user.id ? "You owe" : "Owes"} others`
|
||||
: `Others owe ${
|
||||
user.id === data.user.id ? "you" : "him/her"
|
||||
}`}
|
||||
: `Others owe ${user.id === data.user.id ? "you" : ""}`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue