diff --git a/examples/remix/app/bknd.ts b/examples/remix/app/bknd.ts new file mode 100644 index 0000000..3a71f75 --- /dev/null +++ b/examples/remix/app/bknd.ts @@ -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(); +} diff --git a/examples/remix/app/components/Check.tsx b/examples/remix/app/components/Check.tsx new file mode 100644 index 0000000..912db7d --- /dev/null +++ b/examples/remix/app/components/Check.tsx @@ -0,0 +1,9 @@ +export function Check({ checked = false }: { checked?: boolean }) { + return ( +
+ +
+ ); +} diff --git a/examples/remix/app/root.tsx b/examples/remix/app/root.tsx index 004ec11..6f9258e 100644 --- a/examples/remix/app/root.tsx +++ b/examples/remix/app/root.tsx @@ -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(); @@ -40,7 +36,7 @@ export default function App() { // that you're authed using cookie return ( - + ); } diff --git a/examples/remix/app/routes/_index.tsx b/examples/remix/app/routes/_index.tsx index 4337d0e..4ea76c2 100644 --- a/examples/remix/app/routes/_index.tsx +++ b/examples/remix/app/routes/_index.tsx @@ -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(); + const ctx = useOutletContext(); + const { todos, total, limit } = useLoaderData(); + const fetcher = useFetcher(); return ( -
-

Data

-
{JSON.stringify(data, null, 2)}
-

User

-
{JSON.stringify(user, null, 2)}
+
+
+
+

+ bknd w/ Remix +

+
+ Remix + Remix +
+
+ +
+ Go to Admin ➝ +
+ {ctx.user ? ( +

+ Authenticated as {ctx.user.email} +

+ ) : ( + Login + )} +
+
+
); } diff --git a/examples/remix/app/routes/admin.$.tsx b/examples/remix/app/routes/admin.$.tsx index 5aaa847..77f6747 100644 --- a/examples/remix/app/routes/admin.$.tsx +++ b/examples/remix/app/routes/admin.$.tsx @@ -4,6 +4,7 @@ import "bknd/dist/styles.css"; export default adminPage({ config: { basepath: "/admin", - logo_return_path: "/../" + logo_return_path: "/../", + color_scheme: "system" } }); diff --git a/examples/remix/app/routes/api.$.ts b/examples/remix/app/routes/api.$.ts index 3d1911e..d01a6de 100644 --- a/examples/remix/app/routes/api.$.ts +++ b/examples/remix/app/routes/api.$.ts @@ -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; diff --git a/examples/remix/app/tailwind.css b/examples/remix/app/tailwind.css new file mode 100644 index 0000000..6f64770 --- /dev/null +++ b/examples/remix/app/tailwind.css @@ -0,0 +1,10 @@ +@import "tailwindcss"; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/examples/remix/package.json b/examples/remix/package.json index 2901131..d375498 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -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" - } -} \ No newline at end of file + "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" + } +} diff --git a/examples/remix/public/logo-dark.png b/examples/remix/public/logo-dark.png new file mode 100644 index 0000000..b24c7ae Binary files /dev/null and b/examples/remix/public/logo-dark.png differ diff --git a/examples/remix/public/logo-light.png b/examples/remix/public/logo-light.png new file mode 100644 index 0000000..4490ae7 Binary files /dev/null and b/examples/remix/public/logo-light.png differ diff --git a/examples/remix/vite.config.ts b/examples/remix/vite.config.ts index 3fa98cd..b5632d1 100644 --- a/examples/remix/vite.config.ts +++ b/examples/remix/vite.config.ts @@ -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,