diff --git a/README.md b/README.md index 644f0d0..d5070c9 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Creating digital products always requires developing both the backend (the logic * 🏃‍♂️ Multiple run modes * standalone using the CLI * using a JavaScript runtime (Node, Bun, workerd) - * using a React framework (Astro, Remix, Next.js) + * using a React framework (Next.js, React Router, Astro) * 📦 Official API and React SDK with type-safety * ⚛️ React elements for auto-configured authentication and media components diff --git a/app/build.ts b/app/build.ts index 836a816..1720b2c 100644 --- a/app/build.ts +++ b/app/build.ts @@ -228,7 +228,7 @@ async function buildAdapters() { }); // specific adatpers - await tsup.build(baseConfig("remix")); + await tsup.build(baseConfig("react-router")); await tsup.build(baseConfig("bun")); await tsup.build(baseConfig("astro")); await tsup.build(baseConfig("aws")); diff --git a/app/package.json b/app/package.json index fad1abb..463e9c9 100644 --- a/app/package.json +++ b/app/package.json @@ -3,8 +3,8 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.9.1", - "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", + "version": "0.10.0-rc.3", + "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { "type": "git", @@ -19,7 +19,7 @@ "test:coverage": "ALL_TESTS=1 bun test --bail --coverage", "build": "NODE_ENV=production bun run build.ts --minify --types", "build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli", - "build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify", + "build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --env PUBLIC_* --minify", "build:static": "vite build", "watch": "bun run build.ts --types --watch", "types": "bun tsc -p tsconfig.build.json --noEmit", @@ -108,8 +108,8 @@ "@hono/node-server": "^1.13.8" }, "peerDependencies": { - "react": "^19.x", - "react-dom": "^19.x" + "react": ">=19", + "react-dom": ">=19" }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -174,10 +174,10 @@ "import": "./dist/adapter/nextjs/index.js", "require": "./dist/adapter/nextjs/index.cjs" }, - "./adapter/remix": { - "types": "./dist/types/adapter/remix/index.d.ts", - "import": "./dist/adapter/remix/index.js", - "require": "./dist/adapter/remix/index.cjs" + "./adapter/react-router": { + "types": "./dist/types/adapter/react-router/index.d.ts", + "import": "./dist/adapter/react-router/index.js", + "require": "./dist/adapter/react-router/index.cjs" }, "./adapter/bun": { "types": "./dist/types/adapter/bun/index.d.ts", @@ -227,6 +227,7 @@ "cloudflare", "nextjs", "remix", + "react-router", "astro", "bun", "node" diff --git a/app/src/adapter/react-router/index.ts b/app/src/adapter/react-router/index.ts new file mode 100644 index 0000000..e1e917b --- /dev/null +++ b/app/src/adapter/react-router/index.ts @@ -0,0 +1 @@ +export * from "./react-router.adapter"; diff --git a/app/src/adapter/remix/remix.adapter.ts b/app/src/adapter/react-router/react-router.adapter.ts similarity index 61% rename from app/src/adapter/remix/remix.adapter.ts rename to app/src/adapter/react-router/react-router.adapter.ts index 0820133..7e796c6 100644 --- a/app/src/adapter/remix/remix.adapter.ts +++ b/app/src/adapter/react-router/react-router.adapter.ts @@ -1,18 +1,17 @@ import type { App } from "bknd"; import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; -export type RemixBkndConfig = FrameworkBkndConfig; - -type RemixContext = { +type ReactRouterContext = { request: Request; }; +export type ReactRouterBkndConfig = FrameworkBkndConfig; let app: App; let building: boolean = false; -export async function getApp( - config: RemixBkndConfig, - args?: Args +export async function getApp( + config: ReactRouterBkndConfig, + args?: Args, ) { if (building) { while (building) { @@ -30,8 +29,8 @@ export async function getApp( return app; } -export function serve( - config: RemixBkndConfig = {}, +export function serve( + config: ReactRouterBkndConfig = {}, ) { return async (args: Args) => { app = await getApp(config, args); diff --git a/app/src/adapter/remix/AdminPage.tsx b/app/src/adapter/remix/AdminPage.tsx deleted file mode 100644 index 5c9e90b..0000000 --- a/app/src/adapter/remix/AdminPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useAuth } from "bknd/client"; -import type { BkndAdminProps } from "bknd/ui"; -import { Suspense, lazy, useEffect, useState } from "react"; - -export function adminPage(props?: BkndAdminProps) { - const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin }))); - return () => { - const auth = useAuth(); - const [loaded, setLoaded] = useState(false); - useEffect(() => { - if (typeof window === "undefined") return; - setLoaded(true); - }, []); - if (!loaded) return null; - - return ( - - - - ); - }; -} diff --git a/app/src/adapter/remix/index.ts b/app/src/adapter/remix/index.ts deleted file mode 100644 index e02c2c0..0000000 --- a/app/src/adapter/remix/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./remix.adapter"; -export * from "./AdminPage"; diff --git a/app/src/cli/commands/create/create.ts b/app/src/cli/commands/create/create.ts index 299afbd..478362f 100644 --- a/app/src/cli/commands/create/create.ts +++ b/app/src/cli/commands/create/create.ts @@ -24,7 +24,7 @@ const config = { }, framework: { nextjs: "Next.js", - remix: "Remix", + "react-router": "React Router", astro: "Astro", }, } as const; diff --git a/app/src/cli/commands/create/templates/index.ts b/app/src/cli/commands/create/templates/index.ts index f3cf9b5..563e955 100644 --- a/app/src/cli/commands/create/templates/index.ts +++ b/app/src/cli/commands/create/templates/index.ts @@ -1,6 +1,4 @@ import { cloudflare } from "./cloudflare"; -import { nextjs } from "./nextjs"; -import { remix } from "./remix"; export type TemplateSetupCtx = { template: Template; @@ -13,7 +11,7 @@ export type Integration = | "bun" | "cloudflare" | "nextjs" - | "remix" + | "react-router" | "astro" | "aws" | "custom"; @@ -43,8 +41,6 @@ export type Template = { export const templates: Template[] = [ cloudflare, - nextjs, - remix, { key: "node", title: "Node.js Basic", @@ -61,6 +57,14 @@ export const templates: Template[] = [ path: "gh:bknd-io/bknd/examples/bun", ref: true, }, + { + key: "nextjs", + title: "Next.js Basic", + integration: "nextjs", + description: "A basic bknd Next.js starter", + path: "gh:bknd-io/bknd/examples/nextjs", + ref: true, + }, { key: "astro", title: "Astro Basic", @@ -69,6 +73,14 @@ export const templates: Template[] = [ path: "gh:bknd-io/bknd/examples/astro", ref: true, }, + { + key: "react-router", + title: "React Router Basic", + integration: "react-router", + description: "A basic bknd React Router starter", + path: "gh:bknd-io/bknd/examples/react-router", + ref: true, + }, { key: "aws", title: "AWS Lambda Basic", diff --git a/app/src/cli/commands/create/templates/nextjs.ts b/app/src/cli/commands/create/templates/nextjs.ts deleted file mode 100644 index 94b4b58..0000000 --- a/app/src/cli/commands/create/templates/nextjs.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { overridePackageJson } from "cli/commands/create/npm"; -import type { Template } from "."; - -// @todo: add `concurrently`? -export const nextjs = { - key: "nextjs", - title: "Next.js Basic", - integration: "nextjs", - description: "A basic bknd Next.js starter", - path: "gh:bknd-io/bknd/examples/nextjs", - scripts: { - install: "npm install --force", - }, - ref: true, - preinstall: async (ctx) => { - // locally it's required to overwrite react, here it is not - await overridePackageJson( - (pkg) => ({ - ...pkg, - dependencies: { - ...pkg.dependencies, - react: undefined, - "react-dom": undefined, - }, - }), - { dir: ctx.dir }, - ); - }, -} as const satisfies Template; diff --git a/app/src/cli/commands/create/templates/remix.ts b/app/src/cli/commands/create/templates/remix.ts deleted file mode 100644 index 3eef651..0000000 --- a/app/src/cli/commands/create/templates/remix.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { overridePackageJson } from "cli/commands/create/npm"; -import type { Template } from "."; - -export const remix = { - key: "remix", - title: "Remix Basic", - integration: "remix", - description: "A basic bknd Remix starter", - path: "gh:bknd-io/bknd/examples/remix", - ref: true, - preinstall: async (ctx) => { - // locally it's required to overwrite react - await overridePackageJson( - (pkg) => ({ - ...pkg, - dependencies: { - ...pkg.dependencies, - react: "^18.2.0", - "react-dom": "^18.2.0", - }, - }), - { dir: ctx.dir }, - ); - }, -} as const satisfies Template; diff --git a/app/src/core/env.ts b/app/src/core/env.ts index a32427c..6d4f0ba 100644 --- a/app/src/core/env.ts +++ b/app/src/core/env.ts @@ -37,7 +37,10 @@ const envs = { // cli telemetry cli_telemetry: { key: "BKND_CLI_TELEMETRY", - validate: (v: unknown) => { + validate: (v: unknown): boolean | undefined => { + if (typeof v === "undefined") { + return undefined; + } return is_toggled(v, true); }, }, diff --git a/docs/integration/introduction.mdx b/docs/integration/introduction.mdx index dc5bdea..01741f9 100644 --- a/docs/integration/introduction.mdx +++ b/docs/integration/introduction.mdx @@ -3,7 +3,7 @@ title: 'Introduction' description: 'Integrate bknd into your runtime/framework of choice' --- -import { cloudflare, nextjs, remix, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx" +import { cloudflare, nextjs, reactRouter, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx" ## Start with a Framework bknd seamlessly integrates with popular frameworks, allowing you to use what you're already familar with. The following guides will help you get started with your framework of choice. @@ -15,9 +15,9 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/nextjs" /> {remix}} - href="/integration/remix" + title="React Router" + icon={
{reactRouter}
} + href="/integration/react-router" /> + + Create a new React Router CLI starter project by running the following command: + + ```sh + npx bknd create -i react-router + ``` + + + Create a new React Router project by following the [official guide](https://reactrouter.com/start/framework/installation), and then install bknd as a dependency: + + + + + + +## Serve the API +Create a helper file to instantiate the bknd instance and retrieve the API: + +```ts app/bknd.ts +import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router"; + +const config = { + connection: { + url: "file:data.db" + } +} 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(); +} +``` +For more information about the connection object, refer to the [Database](/usage/database) guide. + +Create a new api splat route file at `app/routes/api.$.ts`: +```ts app/routes/api.$.ts +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; +``` + +## Enabling the Admin UI +Create a new splat route file at `app/routes/admin.$.tsx`: +```tsx app/routes/admin.$.tsx +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(); + // derived from https://github.com/sergiodxa/remix-utils + // @ts-ignore + const hydrated = useSyncExternalStore(() => {}, () => true, () => false); + if (!hydrated) return null; + + return ( + + + + ); +} + +``` + +## Example usage of the API +You can use the `getApi` helper function we've already set up to fetch and mutate: +```tsx app/routes/_index.tsx +import { useLoaderData, type LoaderFunctionArgs } from "react-router"; +import { getApi } from "~/bknd"; + +export const loader = async (args: LoaderFunctionArgs) => { + // use authentication from request + const api = await getApi(args, { verify: true }); + const { data } = await api.data.readMany("todos"); + return { data, user: api.getUser() }; +}; + +export default function Index() { + const { data, user } = useLoaderData(); + + return ( +
+

Data

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

User

+
{JSON.stringify(user, null, 2)}
+
+ ); +} +``` \ No newline at end of file diff --git a/docs/integration/remix.mdx b/docs/integration/remix.mdx deleted file mode 100644 index 48328f5..0000000 --- a/docs/integration/remix.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: 'Remix' -description: 'Run bknd inside Remix' ---- -import InstallBknd from '/snippets/install-bknd.mdx'; - -## Installation -To get started with Remix and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter: - - - - Create a new Remix CLI starter project by running the following command: - - ```sh - npx bknd create -i remix - ``` - - - Create a new Remix project by following the [official guide](https://remix.run/docs/en/main/other-api/create-remix), and then install bknd as a dependency: - - - - - - -## Serve the API -Create a helper file to instantiate the bknd instance and retrieve the API: - -```ts app/bknd.ts -import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix"; - -const config = { - connection: { - url: "file:data.db" - } -} as const satisfies RemixBkndConfig; - -export async function getApp(args?: { request: Request }) { - return await getBkndApp(config, 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(); -} -``` -For more information about the connection object, refer to the [Database](/usage/database) guide. - -Create a new api splat route file at `app/routes/api.$.ts`: -```ts app/routes/api.$.ts -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; -``` - -Now make sure that you wrap your root layout with the `ClientProvider` so that all components share the same context. Also add the user context to both the `Outlet` and the provider: -```tsx app/root.tsx -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { useLoaderData, Outlet } from "@remix-run/react"; -import { ClientProvider } from "bknd/client"; -import { getApi } from "~/bknd"; - -export function Layout(props) { - // nothing to change here, just for orientation - return ( - {/* ... */} - ); -} - -export const loader = async (args: LoaderFunctionArgs) => { - const api = await getApi(args); - return { - user: api.getUser() - }; -}; - -export default function App() { - const data = useLoaderData(); - return ( - - - - ); -} -``` - -## Enabling the Admin UI -Create a new splat route file at `app/routes/admin.$.tsx`: -```tsx app/routes/admin.$.tsx -import { adminPage } from "bknd/adapter/remix"; -import "bknd/dist/styles.css"; - -export default adminPage({ - config: { - basepath: "/admin", - logo_return_path: "/../", - color_scheme: "system" - } -}); -``` - -## Example usage of the API -Since the API has already been constructed in the root layout, you can now use it in any page: -```tsx app/routes/_index.tsx -import { useLoaderData } from "@remix-run/react"; -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { getApi } from "~/bknd"; - -export const loader = async (args: LoaderFunctionArgs) => { - // use authentication from request - const api = await getApi(args); - const { data } = await api.data.readMany("todos"); - return { data, user: api.getUser() }; -}; - -export default function Index() { - const { data, user } = useLoaderData(); - - return ( -
-

Data

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

User

-
{JSON.stringify(user, null, 2)}
-
- ); -} -``` \ No newline at end of file diff --git a/docs/introduction.mdx b/docs/introduction.mdx index cf50d68..7a2ea6a 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -2,7 +2,7 @@ title: Introduction --- -import { cloudflare, nextjs, remix, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx" +import { cloudflare, nextjs, reactRouter, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx" import { Stackblitz, examples } from "/snippets/stackblitz.mdx" Glad you're here! This is about **bknd**, a feature-rich backend that is so lightweight it could @@ -44,9 +44,9 @@ in the future, so stay tuned! href="/integration/nextjs" /> {remix}} - href="/integration/remix" + title="React Router" + icon={
{reactRouter}
} + href="/integration/react-router" /> -export const remix = ; +export const reactRouter = ; export const astro = diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 80059f9..a0fbd4d 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "bknd": "file:../../app", - "react": "file:../../node_modules/react", - "react-dom": "file:../../node_modules/react-dom", + "react": "^19.0.0", + "react-dom": "^19.0.0", "next": "15.2.0" }, "devDependencies": { diff --git a/examples/react-router/.dockerignore b/examples/react-router/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/examples/react-router/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/examples/react-router/.gitignore b/examples/react-router/.gitignore new file mode 100644 index 0000000..261e24f --- /dev/null +++ b/examples/react-router/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/node_modules/ + +.env +*.db + +# React Router +/.react-router/ +/build/ diff --git a/examples/react-router/Dockerfile b/examples/react-router/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/examples/react-router/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/examples/remix/README.md b/examples/react-router/README.md similarity index 83% rename from examples/remix/README.md rename to examples/react-router/README.md index 0c44258..db8640b 100644 --- a/examples/remix/README.md +++ b/examples/react-router/README.md @@ -1,14 +1,15 @@ -# bknd starter: Remix -A minimal Remix project with bknd integration. +# bknd starter: React Router +A minimal React Router project with bknd integration. ## Project Structure -Inside of your Remix project, you'll see the following folders and files: +Inside of your React Router project, you'll see the following folders and files: ```text / ├── public/ ├── app/ +│ ├── bknd.ts │ ├── root.tsx │ └── routes/ │ ├── _index.tsx diff --git a/examples/react-router/app/app.css b/examples/react-router/app/app.css new file mode 100644 index 0000000..99345d8 --- /dev/null +++ b/examples/react-router/app/app.css @@ -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; + } +} diff --git a/examples/remix/app/bknd.ts b/examples/react-router/app/bknd.ts similarity index 82% rename from examples/remix/app/bknd.ts rename to examples/react-router/app/bknd.ts index c8fc146..6819cad 100644 --- a/examples/remix/app/bknd.ts +++ b/examples/react-router/app/bknd.ts @@ -1,6 +1,6 @@ -import { App, type LocalApiOptions } from "bknd"; +import { App } from "bknd"; import { registerLocalMediaAdapter } from "bknd/adapter/node"; -import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix"; +import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router"; import { boolean, em, entity, text } from "bknd/data"; import { secureRandomString } from "bknd/utils"; @@ -70,13 +70,19 @@ const config = { "sync", ); }, -} as const satisfies RemixBkndConfig; +} as const satisfies ReactRouterBkndConfig; export async function getApp(args?: { request: Request }) { return await getBkndApp(config, args); } -export async function getApi(options?: LocalApiOptions) { +export async function getApi(args?: { request: Request }, opts?: { verify?: boolean }) { const app = await getApp(); - return await app.getApi(options); + if (opts?.verify) { + const api = app.getApi({ headers: args?.request.headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); } diff --git a/examples/react-router/app/root.tsx b/examples/react-router/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/examples/react-router/app/root.tsx @@ -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 ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +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 ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/examples/react-router/app/routes.ts b/examples/react-router/app/routes.ts new file mode 100644 index 0000000..30b5e6f --- /dev/null +++ b/examples/react-router/app/routes.ts @@ -0,0 +1,4 @@ +import type { RouteConfig } from "@react-router/dev/routes"; +import { flatRoutes } from "@react-router/fs-routes"; + +export default flatRoutes() satisfies RouteConfig; diff --git a/examples/remix/app/routes/_index.tsx b/examples/react-router/app/routes/_index.tsx similarity index 78% rename from examples/remix/app/routes/_index.tsx rename to examples/react-router/app/routes/_index.tsx index 4ea76c2..938d238 100644 --- a/examples/remix/app/routes/_index.tsx +++ b/examples/react-router/app/routes/_index.tsx @@ -1,80 +1,55 @@ -import { type MetaFunction, useFetcher, useLoaderData, useOutletContext } from "@remix-run/react"; -import type { ActionFunctionArgs } from "@remix-run/server-runtime"; +import type { Route } from "./+types/_index"; +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + useFetcher, + useLoaderData, +} from "react-router"; import { getApi } from "~/bknd"; -export const meta: MetaFunction = () => { +// biome-ignore lint/correctness/noEmptyPattern: +export function meta({}: Route.MetaArgs) { return [ - { title: "New bknd-Remix App" }, - { name: "description", content: "Welcome to bknd & Remix!" } + { title: "New bknd React Router App" }, + { name: "description", content: "Welcome to bknd & React Router!" }, ]; -}; +} -export const loader = async () => { - const api = await getApi(); +export const loader = async (args: LoaderFunctionArgs) => { + const api = await getApi(args, { verify: true }); const limit = 5; const { data: todos, - body: { meta } + body: { meta }, } = await api.data.readMany("todos", { limit, - sort: "-id" + 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; + return { todos: todos.reverse(), total: meta.total, limit, user: api.getUser() }; }; export default function Index() { - const ctx = useOutletContext(); - const { todos, total, limit } = useLoaderData(); + const { todos, total, limit, user } = useLoaderData(); const fetcher = useFetcher(); return (
-

- bknd w/ Remix -

-
- Remix - Remix + bknd +
+ React Router + React Router