updated react example with todo example

This commit is contained in:
dswbx
2025-03-15 16:37:03 +01:00
parent 0e81e14421
commit 2531c2d8d4
15 changed files with 261 additions and 37 deletions

View File

@@ -1,9 +1,13 @@
import { useEffect, useState } from "react";
import { createContext, lazy, useEffect, useState, Suspense, Fragment } from "react";
import { App } from "bknd";
import { Admin } from "bknd/ui";
import { checksum } from "bknd/utils";
import { em, entity, text } from "bknd/data";
import { checksum, secureRandomString } from "bknd/utils";
import { boolean, em, entity, text } from "bknd/data";
import { SQLocalConnection } from "@bknd/sqlocal";
import { Route, Router, Switch } from "wouter";
import IndexPage from "~/routes/_index";
const Admin = lazy(() => import("~/routes/admin"));
import { Center } from "~/components/Center";
import { ClientProvider } from "bknd/client";
import "bknd/dist/styles.css";
export default function () {
@@ -11,28 +15,65 @@ export default function () {
const [hash, setHash] = useState<string>("");
async function onBuilt(app: App) {
setApp(app);
setHash(await checksum(app.toJSON()));
document.startViewTransition(async () => {
setApp(app);
setHash(await checksum(app.toJSON()));
});
}
useEffect(() => {
setup({
onBuilt,
})
setup({ onBuilt })
.then((app) => console.log("setup", app?.version()))
.catch(console.error);
}, []);
if (!app) return null;
if (!app)
return (
<Center>
<span className="opacity-20">Loading...</span>
</Center>
);
return (
// @ts-ignore
<Admin key={hash} withProvider={{ api: app.getApi() }} />
<Router key={hash}>
<Switch>
<Route
path="/"
component={() => (
<ClientProvider api={app.getApi()}>
<IndexPage app={app} />
</ClientProvider>
)}
/>
<Route path="/admin/*?">
<Suspense>
<Admin config={{ basepath: "/admin", logo_return_path: "/../" }} app={app} />
</Suspense>
</Route>
<Route path="*">
<Center className="font-mono text-4xl">404</Center>
</Route>
</Switch>
</Router>
);
}
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 {}
}
let initialized = false;
export async function setup(opts?: {
async function setup(opts?: {
beforeBuild?: (app: App) => Promise<void>;
onBuilt?: (app: App) => Promise<void>;
}) {
@@ -40,17 +81,30 @@ export async function setup(opts?: {
initialized = true;
const connection = new SQLocalConnection({
databasePath: ":localStorage:",
verbose: true,
});
const app = App.create({
connection,
// an initial config is only applied if the database is empty
initialConfig: {
data: em({
test: entity("test", {
name: text(),
}),
}).toJSON(),
data: schema.toJSON(),
},
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 },
]);
// @todo: auth is currently not working due to POST request
/*await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});*/
},
},
});

View File

@@ -0,0 +1,10 @@
import type { ComponentProps } from "react";
export function Center(props: ComponentProps<"div">) {
return (
<div
{...props}
className={"w-full min-h-full flex justify-center items-center " + props.className}
/>
);
}

View File

@@ -1,6 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import App from "./App";
import "./styles.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>

View File

@@ -0,0 +1,94 @@
import { Center } from "~/components/Center";
import type { App } from "bknd";
import { useEntityQuery } from "bknd/client";
export default function IndexPage({ app }: { app: App }) {
const user = app.getApi().getUser();
const limit = 5;
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
limit,
});
// @ts-ignore
const total = todos?.body.meta.total || 0;
return (
<div id="app">
<Center className="flex-col gap-10 max-w-96 mx-auto text-lg">
<div className="flex flex-col gap-2 items-center">
<img src="/bknd.svg" alt="bknd" className="w-48 dark:invert" />
<p className="font-mono">local</p>
</div>
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
<h2 className="font-mono mb-1 opacity-70">
<code>What's next? ({total})</code>
</h2>
<div className="flex flex-col w-full gap-2">
{total > limit && (
<div className="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
{total - limit} more todo(s) hidden
</div>
)}
<div className="flex flex-col gap-3">
{todos?.reverse().map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
className="flex-shrink-0 cursor-pointer"
defaultChecked={!!todo.done}
onChange={async () => {
await $q.update({ done: !todo.done }, todo.id);
}}
/>
<div className="text-foreground/90 leading-none">{todo.title}</div>
</div>
<button
type="button"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
onClick={async () => {
await $q._delete(todo.id);
}}
>
</button>
</div>
))}
</div>
<form
className="flex flex-row w-full gap-3 mt-2"
key={todos?.map((t) => t.id).join()}
action={async (formData: FormData) => {
const title = formData.get("title") as string;
await $q.create({ title });
}}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
/>
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
</div>
</div>
<div className="flex flex-col items-center gap-1">
<a href="/admin">Go to Admin </a>
{/*<div className="opacity-50 text-sm">
{user ? (
<p>
Authenticated as <b>{user.email}</b>
</p>
) : (
<a href="/admin/auth/login">Login</a>
)}
</div>*/}
</div>
</Center>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { Admin, type BkndAdminProps } from "bknd/ui";
import type { App } from "bknd";
export default function AdminPage({
app,
...props
}: Omit<BkndAdminProps, "withProvider"> & { app: App }) {
return <Admin {...props} withProvider={{ api: app.getApi() }} />;
}

View File

@@ -0,0 +1,25 @@
/* @todo: currently not working nicely */
#app {
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
width: 100%;
min-height: 100dvh;
@apply bg-background text-foreground flex;
font-family: Arial, Helvetica, sans-serif;
}