mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 12:56:05 +00:00
replaced remix with react-router
This commit is contained in:
15
examples/react-router/app/app.css
Normal file
15
examples/react-router/app/app.css
Normal file
@@ -0,0 +1,15 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-950;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
88
examples/react-router/app/bknd.ts
Normal file
88
examples/react-router/app/bknd.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { App } from "bknd";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
|
||||
import { boolean, em, entity, text } from "bknd/data";
|
||||
import { secureRandomString } from "bknd/utils";
|
||||
|
||||
// since we're running in node, we can register the local media adapter
|
||||
registerLocalMediaAdapter();
|
||||
|
||||
const schema = em({
|
||||
todos: entity("todos", {
|
||||
title: text(),
|
||||
done: boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
// register your schema to get automatic type completion
|
||||
type Database = (typeof schema)["DB"];
|
||||
declare module "bknd/core" {
|
||||
interface DB extends Database {}
|
||||
}
|
||||
|
||||
const config = {
|
||||
// we can use any libsql config, and if omitted, uses in-memory
|
||||
connection: {
|
||||
url: "file:test.db",
|
||||
},
|
||||
// an initial config is only applied if the database is empty
|
||||
initialConfig: {
|
||||
data: schema.toJSON(),
|
||||
// we're enabling auth ...
|
||||
auth: {
|
||||
enabled: true,
|
||||
jwt: {
|
||||
issuer: "bknd-remix-example",
|
||||
secret: secureRandomString(64),
|
||||
},
|
||||
},
|
||||
// ... and media
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./public",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
// the seed option is only executed if the database was empty
|
||||
seed: async (ctx) => {
|
||||
await ctx.em.mutator("todos").insertMany([
|
||||
{ title: "Learn bknd", done: true },
|
||||
{ title: "Build something cool", done: false },
|
||||
]);
|
||||
},
|
||||
},
|
||||
// here we can hook into the app lifecycle events ...
|
||||
beforeBuild: async (app) => {
|
||||
app.emgr.onEvent(
|
||||
App.Events.AppFirstBoot,
|
||||
async () => {
|
||||
// ... to create an initial user
|
||||
await app.module.auth.createUser({
|
||||
email: "test@bknd.io",
|
||||
password: "12345678",
|
||||
});
|
||||
},
|
||||
"sync",
|
||||
);
|
||||
},
|
||||
} as const satisfies ReactRouterBkndConfig;
|
||||
|
||||
export async function getApp(args?: { request: Request }) {
|
||||
return await getBkndApp(config, args);
|
||||
}
|
||||
|
||||
export async function getApi(args?: { request: Request }, opts?: { verify?: boolean }) {
|
||||
const app = await getApp();
|
||||
if (opts?.verify) {
|
||||
const api = app.getApi({ headers: args?.request.headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
}
|
||||
75
examples/react-router/app/root.tsx
Normal file
75
examples/react-router/app/root.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "react-router";
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
4
examples/react-router/app/routes.ts
Normal file
4
examples/react-router/app/routes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
162
examples/react-router/app/routes/_index.tsx
Normal file
162
examples/react-router/app/routes/_index.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
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'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;
|
||||
};
|
||||
31
examples/react-router/app/routes/admin.$.tsx
Normal file
31
examples/react-router/app/routes/admin.$.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { lazy, Suspense, useSyncExternalStore } from "react";
|
||||
import { type LoaderFunctionArgs, useLoaderData } from "react-router";
|
||||
import { getApi } from "~/bknd";
|
||||
|
||||
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
|
||||
import "bknd/dist/styles.css";
|
||||
|
||||
export const loader = async (args: LoaderFunctionArgs) => {
|
||||
const api = await getApi(args, { verify: true });
|
||||
return {
|
||||
user: api.getUser(),
|
||||
};
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
const { user } = useLoaderData<typeof loader>();
|
||||
// derived from https://github.com/sergiodxa/remix-utils
|
||||
const hydrated = useSyncExternalStore(
|
||||
// @ts-ignore
|
||||
() => {},
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (!hydrated) return null;
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Admin withProvider={{ user }} config={{ basepath: "/admin", logo_return_path: "/../" }} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
9
examples/react-router/app/routes/api.$.ts
Normal file
9
examples/react-router/app/routes/api.$.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getApp } from "~/bknd";
|
||||
|
||||
const handler = async (args: { request: Request }) => {
|
||||
const app = await getApp(args);
|
||||
return app.fetch(args.request);
|
||||
};
|
||||
|
||||
export const loader = handler;
|
||||
export const action = handler;
|
||||
Reference in New Issue
Block a user