Files
bknd/examples/react-router/app/routes/_index.tsx
2025-03-14 15:32:43 +01:00

163 lines
5.8 KiB
TypeScript

import type { Route } from "./+types/_index";
import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
useFetcher,
useLoaderData,
} from "react-router";
import { getApi } from "~/bknd";
// biome-ignore lint/correctness/noEmptyPattern: <explanation>
export function meta({}: Route.MetaArgs) {
return [
{ title: "New bknd React Router App" },
{ name: "description", content: "Welcome to bknd & React Router!" },
];
}
export const loader = async (args: LoaderFunctionArgs) => {
const api = await getApi(args, { verify: true });
const limit = 5;
const {
data: todos,
body: { meta },
} = await api.data.readMany("todos", {
limit,
sort: "-id",
});
return { todos: todos.reverse(), total: meta.total, limit, user: api.getUser() };
};
export default function Index() {
const { todos, total, limit, user } = useLoaderData<typeof loader>();
const fetcher = useFetcher();
return (
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center gap-16">
<header className="flex flex-col items-center gap-9">
<img src="/bknd.svg" alt="bknd" className="block w-48 dark:invert" />
<div className="h-[144px] w-96">
<img
src="/logo-light.svg"
alt="React Router"
className="block w-full dark:hidden"
/>
<img
src="/logo-dark.svg"
alt="React Router"
className="hidden w-full dark:block"
/>
</div>
</header>
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
<p className="leading-6 text-gray-700 dark:text-gray-200 font-bold">
What&apos;s next? ({total})
</p>
<div className="flex flex-col w-full gap-2">
{total > limit && (
<div className="bg-white/10 flex justify-center p-1 text-xs rounded text-gray-500">
{total - limit} more todo(s) hidden
</div>
)}
{todos.map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<fetcher.Form
className="flex flex-row flex-grow items-center gap-3 ml-1"
method="post"
>
<input
type="checkbox"
name="done"
defaultChecked={todo.done}
onChange={(e) => fetcher.submit(e.currentTarget.form!)}
/>
<input type="hidden" name="action" value="update" />
<input type="hidden" name="id" value={String(todo.id)} />
<div className="dark:text-gray-300 text-gray-800">{todo.title}</div>
</fetcher.Form>
<fetcher.Form className="flex items-center" method="post">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="id" value={String(todo.id)} />
<button
type="submit"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
>
</button>
</fetcher.Form>
</div>
))}
<fetcher.Form
className="flex flex-row gap-3 mt-2"
method="post"
key={todos.map((t) => t.id).join()}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 rounded-xl bg-black/5 dark:bg-white/10"
/>
<input type="hidden" name="action" value="add" />
<button type="submit" className="cursor-pointer">
Add
</button>
</fetcher.Form>
</div>
</nav>
<div className="flex flex-col items-center gap-4">
<a href="/admin">Go to Admin </a>
<div className="opacity-50 text-xs">
{user ? (
<p>
Authenticated as <b>{user.email}</b>
</p>
) : (
<a href="/admin/auth/login">Login</a>
)}
</div>
</div>
</div>
</div>
);
}
export const action = async (args: ActionFunctionArgs) => {
const api = await getApi();
const formData = await args.request.formData();
const action = formData.get("action") as string;
switch (action) {
case "update": {
const id = Number(formData.get("id"));
const done = formData.get("done") === "on";
if (id > 0) {
await api.data.updateOne("todos", id, { done });
}
break;
}
case "add": {
const title = formData.get("title") as string;
if (title.length > 0) {
await api.data.createOne("todos", { title });
}
break;
}
case "delete": {
const id = Number(formData.get("id"));
if (id > 0) {
await api.data.deleteOne("todos", id);
}
break;
}
}
return null;
};