adjusted remix example

This commit is contained in:
dswbx
2025-02-19 14:12:40 +01:00
parent 335fe42a22
commit f2e5815e24
11 changed files with 300 additions and 128 deletions

View File

@@ -0,0 +1,92 @@
import { App } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix";
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: "ds@bknd.io",
password: "12345678"
});
},
"sync"
);
}
} as const satisfies RemixBkndConfig;
export async function getApp(args?: { request: Request }) {
return await getBkndApp(config, args);
}
/**
* If args are given, it will use authentication details from the request
* @param args
*/
export async function getApi(args?: { request: Request }) {
const app = await getApp(args);
if (args) {
const api = app.getApi(args.request);
await api.verifyAuth();
return api;
}
return app.getApi();
}

View File

@@ -0,0 +1,9 @@
export function Check({ checked = false }: { checked?: boolean }) {
return (
<div
className={`aspect-square w-6 leading-none rounded-full p-px transition-colors cursor-pointer ${checked ? "bg-green-500" : "bg-white/20 hover:bg-white/40"}`}
>
<input type="checkbox" checked={checked} readOnly />
</div>
);
}

View File

@@ -1,7 +1,8 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react";
import { withApi } from "bknd/adapter/remix";
import { type Api, ClientProvider } from "bknd/client";
import { ClientProvider } from "bknd/client";
import "./tailwind.css";
import { getApi } from "~/bknd";
export function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -21,17 +22,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}
declare module "@remix-run/server-runtime" {
export interface AppLoadContext {
api: Api;
}
}
export const loader = withApi(async (args: LoaderFunctionArgs, api: Api) => {
export const loader = async (args: LoaderFunctionArgs) => {
const api = await getApi(args);
return {
user: api.getUser()
};
});
};
export default function App() {
const data = useLoaderData<typeof loader>();
@@ -40,7 +36,7 @@ export default function App() {
// that you're authed using cookie
return (
<ClientProvider user={data.user}>
<Outlet />
<Outlet context={data} />
</ClientProvider>
);
}

View File

@@ -1,24 +1,151 @@
import { type MetaFunction, useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { type MetaFunction, useFetcher, useLoaderData, useOutletContext } from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { getApi } from "~/bknd";
export const meta: MetaFunction = () => {
return [{ title: "Remix & bknd" }, { name: "description", content: "Welcome to Remix & bknd!" }];
return [
{ title: "New bknd-Remix App" },
{ name: "description", content: "Welcome to bknd & Remix!" }
];
};
export const loader = async ({ context: { api } }: LoaderFunctionArgs) => {
const { data } = await api.data.readMany("todos");
return { data, user: api.getUser() };
export const loader = async () => {
const api = await getApi();
const limit = 5;
const {
data: todos,
body: { meta }
} = await api.data.readMany("todos", {
limit,
sort: "-id"
});
return { todos: todos.reverse(), total: meta.total, limit };
};
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;
};
export default function Index() {
const { data, user } = useLoaderData<typeof loader>();
const ctx = useOutletContext<any>();
const { todos, total, limit } = useLoaderData<typeof loader>();
const fetcher = useFetcher();
return (
<div>
<h1>Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h1>User</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
<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">
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
bknd w/ <span className="sr-only">Remix</span>
</h1>
<div className="h-[144px] w-[434px]">
<img src="/logo-light.png" alt="Remix" className="block w-full dark:hidden" />
<img src="/logo-dark.png" alt="Remix" 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">
{ctx.user ? (
<p>
Authenticated as <b>{ctx.user.email}</b>
</p>
) : (
<a href="/admin/auth/login">Login</a>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import "bknd/dist/styles.css";
export default adminPage({
config: {
basepath: "/admin",
logo_return_path: "/../"
logo_return_path: "/../",
color_scheme: "system"
}
});

View File

@@ -1,76 +1,9 @@
import { App } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { serve } from "bknd/adapter/remix";
import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils";
import { getApp } from "~/bknd";
// 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 handler = serve({
// 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: "ds@bknd.io",
password: "12345678"
});
},
"sync"
);
}
});
const handler = async (args: { request: Request }) => {
const app = await getApp(args);
return app.fetch(args.request);
};
export const loader = handler;
export const action = handler;

View File

@@ -0,0 +1,10 @@
@import "tailwindcss";
html,
body {
@apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}

View File

@@ -1,33 +1,35 @@
{
"name": "remix",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@remix-run/node": "^2.15.2",
"@remix-run/react": "^2.15.2",
"@remix-run/serve": "^2.15.2",
"bknd": "file:../../app",
"isbot": "^5.1.18",
"react": "file:../../node_modules/react",
"react-dom": "file:../../node_modules/react-dom",
"remix-utils": "^7.0.0"
},
"devDependencies": {
"@remix-run/dev": "^2.15.2",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=20.0.0"
}
}
"name": "remix",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@remix-run/node": "^2.15.2",
"@remix-run/react": "^2.15.2",
"@remix-run/serve": "^2.15.2",
"bknd": "file:../../app",
"isbot": "^5.1.18",
"react": "file:../../node_modules/react",
"react-dom": "file:../../node_modules/react-dom",
"remix-utils": "^7.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.7",
"tailwindcss": "^4.0.7",
"@remix-run/dev": "^2.15.2",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=20.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1,4 +1,5 @@
import { vitePlugin as remix } from "@remix-run/dev";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
@@ -10,6 +11,7 @@ declare module "@remix-run/node" {
export default defineConfig({
plugins: [
tailwindcss(),
remix({
future: {
v3_fetcherPersist: true,