mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
replaced remix with react-router
This commit is contained in:
@@ -42,7 +42,7 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* 🏃♂️ Multiple run modes
|
* 🏃♂️ Multiple run modes
|
||||||
* standalone using the CLI
|
* standalone using the CLI
|
||||||
* using a JavaScript runtime (Node, Bun, workerd)
|
* 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
|
* 📦 Official API and React SDK with type-safety
|
||||||
* ⚛️ React elements for auto-configured authentication and media components
|
* ⚛️ React elements for auto-configured authentication and media components
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ async function buildAdapters() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
await tsup.build(baseConfig("remix"));
|
await tsup.build(baseConfig("react-router"));
|
||||||
await tsup.build(baseConfig("bun"));
|
await tsup.build(baseConfig("bun"));
|
||||||
await tsup.build(baseConfig("astro"));
|
await tsup.build(baseConfig("astro"));
|
||||||
await tsup.build(baseConfig("aws"));
|
await tsup.build(baseConfig("aws"));
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.9.1",
|
"version": "0.10.0-rc.3",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"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",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
"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: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",
|
"build:static": "vite build",
|
||||||
"watch": "bun run build.ts --types --watch",
|
"watch": "bun run build.ts --types --watch",
|
||||||
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
"types": "bun tsc -p tsconfig.build.json --noEmit",
|
||||||
@@ -108,8 +108,8 @@
|
|||||||
"@hono/node-server": "^1.13.8"
|
"@hono/node-server": "^1.13.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.x",
|
"react": ">=19",
|
||||||
"react-dom": "^19.x"
|
"react-dom": ">=19"
|
||||||
},
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -174,10 +174,10 @@
|
|||||||
"import": "./dist/adapter/nextjs/index.js",
|
"import": "./dist/adapter/nextjs/index.js",
|
||||||
"require": "./dist/adapter/nextjs/index.cjs"
|
"require": "./dist/adapter/nextjs/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/remix": {
|
"./adapter/react-router": {
|
||||||
"types": "./dist/types/adapter/remix/index.d.ts",
|
"types": "./dist/types/adapter/react-router/index.d.ts",
|
||||||
"import": "./dist/adapter/remix/index.js",
|
"import": "./dist/adapter/react-router/index.js",
|
||||||
"require": "./dist/adapter/remix/index.cjs"
|
"require": "./dist/adapter/react-router/index.cjs"
|
||||||
},
|
},
|
||||||
"./adapter/bun": {
|
"./adapter/bun": {
|
||||||
"types": "./dist/types/adapter/bun/index.d.ts",
|
"types": "./dist/types/adapter/bun/index.d.ts",
|
||||||
@@ -227,6 +227,7 @@
|
|||||||
"cloudflare",
|
"cloudflare",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
"remix",
|
"remix",
|
||||||
|
"react-router",
|
||||||
"astro",
|
"astro",
|
||||||
"bun",
|
"bun",
|
||||||
"node"
|
"node"
|
||||||
|
|||||||
1
app/src/adapter/react-router/index.ts
Normal file
1
app/src/adapter/react-router/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./react-router.adapter";
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
|
|
||||||
export type RemixBkndConfig<Args = RemixContext> = FrameworkBkndConfig<Args>;
|
type ReactRouterContext = {
|
||||||
|
|
||||||
type RemixContext = {
|
|
||||||
request: Request;
|
request: Request;
|
||||||
};
|
};
|
||||||
|
export type ReactRouterBkndConfig<Args = ReactRouterContext> = FrameworkBkndConfig<Args>;
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
let building: boolean = false;
|
let building: boolean = false;
|
||||||
|
|
||||||
export async function getApp<Args extends RemixContext = RemixContext>(
|
export async function getApp<Args extends ReactRouterContext = ReactRouterContext>(
|
||||||
config: RemixBkndConfig<Args>,
|
config: ReactRouterBkndConfig<Args>,
|
||||||
args?: Args
|
args?: Args,
|
||||||
) {
|
) {
|
||||||
if (building) {
|
if (building) {
|
||||||
while (building) {
|
while (building) {
|
||||||
@@ -30,8 +29,8 @@ export async function getApp<Args extends RemixContext = RemixContext>(
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Args extends RemixContext = RemixContext>(
|
export function serve<Args extends ReactRouterContext = ReactRouterContext>(
|
||||||
config: RemixBkndConfig<Args> = {},
|
config: ReactRouterBkndConfig<Args> = {},
|
||||||
) {
|
) {
|
||||||
return async (args: Args) => {
|
return async (args: Args) => {
|
||||||
app = await getApp(config, args);
|
app = await getApp(config, args);
|
||||||
@@ -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 (
|
|
||||||
<Suspense>
|
|
||||||
<Admin withProvider={{ user: auth.user }} {...props} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./remix.adapter";
|
|
||||||
export * from "./AdminPage";
|
|
||||||
@@ -24,7 +24,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
framework: {
|
framework: {
|
||||||
nextjs: "Next.js",
|
nextjs: "Next.js",
|
||||||
remix: "Remix",
|
"react-router": "React Router",
|
||||||
astro: "Astro",
|
astro: "Astro",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { cloudflare } from "./cloudflare";
|
import { cloudflare } from "./cloudflare";
|
||||||
import { nextjs } from "./nextjs";
|
|
||||||
import { remix } from "./remix";
|
|
||||||
|
|
||||||
export type TemplateSetupCtx = {
|
export type TemplateSetupCtx = {
|
||||||
template: Template;
|
template: Template;
|
||||||
@@ -13,7 +11,7 @@ export type Integration =
|
|||||||
| "bun"
|
| "bun"
|
||||||
| "cloudflare"
|
| "cloudflare"
|
||||||
| "nextjs"
|
| "nextjs"
|
||||||
| "remix"
|
| "react-router"
|
||||||
| "astro"
|
| "astro"
|
||||||
| "aws"
|
| "aws"
|
||||||
| "custom";
|
| "custom";
|
||||||
@@ -43,8 +41,6 @@ export type Template = {
|
|||||||
|
|
||||||
export const templates: Template[] = [
|
export const templates: Template[] = [
|
||||||
cloudflare,
|
cloudflare,
|
||||||
nextjs,
|
|
||||||
remix,
|
|
||||||
{
|
{
|
||||||
key: "node",
|
key: "node",
|
||||||
title: "Node.js Basic",
|
title: "Node.js Basic",
|
||||||
@@ -61,6 +57,14 @@ export const templates: Template[] = [
|
|||||||
path: "gh:bknd-io/bknd/examples/bun",
|
path: "gh:bknd-io/bknd/examples/bun",
|
||||||
ref: true,
|
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",
|
key: "astro",
|
||||||
title: "Astro Basic",
|
title: "Astro Basic",
|
||||||
@@ -69,6 +73,14 @@ export const templates: Template[] = [
|
|||||||
path: "gh:bknd-io/bknd/examples/astro",
|
path: "gh:bknd-io/bknd/examples/astro",
|
||||||
ref: true,
|
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",
|
key: "aws",
|
||||||
title: "AWS Lambda Basic",
|
title: "AWS Lambda Basic",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -37,7 +37,10 @@ const envs = {
|
|||||||
// cli telemetry
|
// cli telemetry
|
||||||
cli_telemetry: {
|
cli_telemetry: {
|
||||||
key: "BKND_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);
|
return is_toggled(v, true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: 'Introduction'
|
|||||||
description: 'Integrate bknd into your runtime/framework of choice'
|
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
|
## 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.
|
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"
|
href="/integration/nextjs"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Remix"
|
title="React Router"
|
||||||
icon={<div className="text-primary-light">{remix}</div>}
|
icon={<div className="text-primary-light">{reactRouter}</div>}
|
||||||
href="/integration/remix"
|
href="/integration/react-router"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Astro"
|
title="Astro"
|
||||||
|
|||||||
129
docs/integration/react-router.mdx
Normal file
129
docs/integration/react-router.mdx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
title: 'React Router'
|
||||||
|
description: 'Run bknd inside React Router'
|
||||||
|
---
|
||||||
|
import InstallBknd from '/snippets/install-bknd.mdx';
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
To get started with React Router and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="CLI Starter">
|
||||||
|
Create a new React Router CLI starter project by running the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx bknd create -i react-router
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Manual">
|
||||||
|
Create a new React Router project by following the [official guide](https://reactrouter.com/start/framework/installation), and then install bknd as a dependency:
|
||||||
|
|
||||||
|
<InstallBknd />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
## 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<typeof loader>();
|
||||||
|
// derived from https://github.com/sergiodxa/remix-utils
|
||||||
|
// @ts-ignore
|
||||||
|
const hydrated = useSyncExternalStore(() => {}, () => true, () => false);
|
||||||
|
if (!hydrated) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<Admin withProvider={{ user }} config={{ basepath: "/admin", logo_return_path: "/../" }} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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<typeof loader>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Data</h1>
|
||||||
|
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
<h1>User</h1>
|
||||||
|
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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:
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="CLI Starter">
|
|
||||||
Create a new Remix CLI starter project by running the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npx bknd create -i remix
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Manual">
|
|
||||||
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:
|
|
||||||
|
|
||||||
<InstallBknd />
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
|
|
||||||
## 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 (
|
|
||||||
<html>{/* ... */}</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loader = async (args: LoaderFunctionArgs) => {
|
|
||||||
const api = await getApi(args);
|
|
||||||
return {
|
|
||||||
user: api.getUser()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const data = useLoaderData<typeof loader>();
|
|
||||||
return (
|
|
||||||
<ClientProvider user={data.user}>
|
|
||||||
<Outlet context={data} />
|
|
||||||
</ClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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<typeof loader>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Data</h1>
|
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
||||||
<h1>User</h1>
|
|
||||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
title: Introduction
|
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"
|
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
|
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"
|
href="/integration/nextjs"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Remix"
|
title="React Router"
|
||||||
icon={<div className="text-primary-light">{remix}</div>}
|
icon={<div className="text-primary-light">{reactRouter}</div>}
|
||||||
href="/integration/remix"
|
href="/integration/react-router"
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Astro"
|
title="Astro"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
"group": "Frameworks",
|
"group": "Frameworks",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integration/nextjs",
|
"integration/nextjs",
|
||||||
"integration/remix",
|
"integration/react-router",
|
||||||
"integration/astro",
|
"integration/astro",
|
||||||
"integration/vite"
|
"integration/vite"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ export const nextjs = <svg xmlns="http://www.w3.org/2000/svg" width="28px" heigh
|
|||||||
stroke-linecap="round" stroke-linejoin="round"
|
stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2" d="M9 15V9l7.745 10.65A9 9 0 1 1 19 17.657M15 12V9"/></svg>
|
stroke-width="2" d="M9 15V9l7.745 10.65A9 9 0 1 1 19 17.657M15 12V9"/></svg>
|
||||||
|
|
||||||
export const remix = <svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px"
|
export const reactRouter = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 140"><path d="M78.066 92.588c12.818 0 23.209-10.391 23.209-23.21c0-12.817-10.391-23.208-23.21-23.208c-12.817 0-23.208 10.39-23.208 23.209s10.391 23.209 23.209 23.209m-54.857 46.417c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.391-23.208-23.21-23.208C10.392 92.588 0 102.978 0 115.796s10.39 23.21 23.209 23.21m209.582 0c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.39-23.208-23.209-23.208s-23.209 10.39-23.209 23.208s10.391 23.21 23.21 23.21"/><path fill="currentColor" d="M156.565 70.357c-.742-7.754-1.12-14.208-7.06-18.744c-7.522-5.744-16.044-2.017-26.54-5.806C112.65 43.312 105 34.155 105 23.24C105 10.405 115.578 0 128.626 0c9.665 0 17.974 5.707 21.634 13.883c5.601 10.64 1.96 21.467 8.998 26.921c8.333 6.458 19.568 1.729 32.104 7.848a23.6 23.6 0 0 1 9.84 8.425A22.86 22.86 0 0 1 205 69.718c0 10.915-7.65 20.073-17.964 22.568c-10.497 3.789-19.019.062-26.541 5.806c-8.46 6.46-3.931 17.
|
||||||
viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M19.932
|
267-10.826 28.682c-3.913 7.518-11.867 12.663-21.043 12.663c-13.048 0-23.626-10.405-23.626-23.24c0-9.323 5.582-17.364 13.638-21.066c12.536-6.12 23.77-1.39 32.104-7.848c4.807-3.726 5.823-9.473 5.823-16.926"/></svg>;
|
||||||
17.424c.18 2.31.18 3.394.18 4.576h-5.35c0-.258.004-.493.009-.732c.014-.743.03-1.517-.09-3.081c-.16-2.29-1.147-2.799-2.961-2.799H3.305v-4.166h8.67c2.291 0 3.437-.696 3.437-2.54c0-1.623-1.146-2.605-3.437-2.605h-8.67V2h9.624c5.189 0 7.767 2.449 7.767 6.36c0 2.926-1.814 4.834-4.265 5.152c2.069.413 3.278 1.59 3.501 3.912" clip-rule="evenodd"/><path fill="currentColor" d="M3.305 22v-3.106h5.657c.945 0 1.15.7 1.15 1.118V22z"/></svg>;
|
|
||||||
|
|
||||||
export const astro = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
export const astro = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<rect width="24" height="24" fill="none"/><path fill="currentColor" d="M9.24 19.035c-.901-.826-1.164-2.561-.789-3.819c.65.793 1.552 1.044 2.486 1.186c1.44.218 2.856.137 4.195-.524c.153-.076.295-.177.462-.278c.126.365.159.734.115 1.11c-.107.915-.56 1.622-1.283 2.158c-.289.215-.594.406-.892.608c-.916.622-1.164 1.35-.82 2.41l.034.114a2.4 2.4 0 0 1-1.07-.918a2.6 2.6 0 0 1-.412-1.401c-.003-.248-.003-.497-.036-.74c-.081-.595-.36-.86-.883-.876a1.034 1.034 0 0 0-1.075.843q-.013.058-.033.126M4.1 15.007s2.666-1.303 5.34-1.303l2.016-6.26c.075-.304.296-.51.544-.51c.25 0 .47.206.545.51l2.016 6.26c3.167 0 5.34 1.303 5.34 1.303L15.363 2.602c-.13-.366-.35-.602-.645-.602H9.283c-.296 0-.506.236-.645.602c-.01.024-4.538 12.405-4.538 12.405"/>
|
<rect width="24" height="24" fill="none"/><path fill="currentColor" d="M9.24 19.035c-.901-.826-1.164-2.561-.789-3.819c.65.793 1.552 1.044 2.486 1.186c1.44.218 2.856.137 4.195-.524c.153-.076.295-.177.462-.278c.126.365.159.734.115 1.11c-.107.915-.56 1.622-1.283 2.158c-.289.215-.594.406-.892.608c-.916.622-1.164 1.35-.82 2.41l.034.114a2.4 2.4 0 0 1-1.07-.918a2.6 2.6 0 0 1-.412-1.401c-.003-.248-.003-.497-.036-.74c-.081-.595-.36-.86-.883-.876a1.034 1.034 0 0 0-1.075.843q-.013.058-.033.126M4.1 15.007s2.666-1.303 5.34-1.303l2.016-6.26c.075-.304.296-.51.544-.51c.25 0 .47.206.545.51l2.016 6.26c3.167 0 5.34 1.303 5.34 1.303L15.363 2.602c-.13-.366-.35-.602-.645-.602H9.283c-.296 0-.506.236-.645.602c-.01.024-4.538 12.405-4.538 12.405"/>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bknd": "file:../../app",
|
"bknd": "file:../../app",
|
||||||
"react": "file:../../node_modules/react",
|
"react": "^19.0.0",
|
||||||
"react-dom": "file:../../node_modules/react-dom",
|
"react-dom": "^19.0.0",
|
||||||
"next": "15.2.0"
|
"next": "15.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
4
examples/react-router/.dockerignore
Normal file
4
examples/react-router/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
9
examples/react-router/.gitignore
vendored
Normal file
9
examples/react-router/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
22
examples/react-router/Dockerfile
Normal file
22
examples/react-router/Dockerfile
Normal file
@@ -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"]
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
# bknd starter: Remix
|
# bknd starter: React Router
|
||||||
A minimal Remix project with bknd integration.
|
A minimal React Router project with bknd integration.
|
||||||
|
|
||||||
## Project Structure
|
## 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
|
```text
|
||||||
/
|
/
|
||||||
├── public/
|
├── public/
|
||||||
├── app/
|
├── app/
|
||||||
|
│ ├── bknd.ts
|
||||||
│ ├── root.tsx
|
│ ├── root.tsx
|
||||||
│ └── routes/
|
│ └── routes/
|
||||||
│ ├── _index.tsx
|
│ ├── _index.tsx
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { App, type LocalApiOptions } from "bknd";
|
import { App } from "bknd";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
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 { boolean, em, entity, text } from "bknd/data";
|
||||||
import { secureRandomString } from "bknd/utils";
|
import { secureRandomString } from "bknd/utils";
|
||||||
|
|
||||||
@@ -70,13 +70,19 @@ const config = {
|
|||||||
"sync",
|
"sync",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
} as const satisfies RemixBkndConfig;
|
} as const satisfies ReactRouterBkndConfig;
|
||||||
|
|
||||||
export async function getApp(args?: { request: Request }) {
|
export async function getApp(args?: { request: Request }) {
|
||||||
return await getBkndApp(config, args);
|
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();
|
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();
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -1,80 +1,55 @@
|
|||||||
import { type MetaFunction, useFetcher, useLoaderData, useOutletContext } from "@remix-run/react";
|
import type { Route } from "./+types/_index";
|
||||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
import {
|
||||||
|
type ActionFunctionArgs,
|
||||||
|
type LoaderFunctionArgs,
|
||||||
|
useFetcher,
|
||||||
|
useLoaderData,
|
||||||
|
} from "react-router";
|
||||||
import { getApi } from "~/bknd";
|
import { getApi } from "~/bknd";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
// biome-ignore lint/correctness/noEmptyPattern: <explanation>
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "New bknd-Remix App" },
|
{ title: "New bknd React Router App" },
|
||||||
{ name: "description", content: "Welcome to bknd & Remix!" }
|
{ name: "description", content: "Welcome to bknd & React Router!" },
|
||||||
];
|
];
|
||||||
};
|
}
|
||||||
|
|
||||||
export const loader = async () => {
|
export const loader = async (args: LoaderFunctionArgs) => {
|
||||||
const api = await getApi();
|
const api = await getApi(args, { verify: true });
|
||||||
|
|
||||||
const limit = 5;
|
const limit = 5;
|
||||||
const {
|
const {
|
||||||
data: todos,
|
data: todos,
|
||||||
body: { meta }
|
body: { meta },
|
||||||
} = await api.data.readMany("todos", {
|
} = await api.data.readMany("todos", {
|
||||||
limit,
|
limit,
|
||||||
sort: "-id"
|
sort: "-id",
|
||||||
});
|
});
|
||||||
|
|
||||||
return { todos: todos.reverse(), total: meta.total, limit };
|
return { todos: todos.reverse(), total: meta.total, limit, user: api.getUser() };
|
||||||
};
|
|
||||||
|
|
||||||
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() {
|
export default function Index() {
|
||||||
const ctx = useOutletContext<any>();
|
const { todos, total, limit, user } = useLoaderData<typeof loader>();
|
||||||
const { todos, total, limit } = useLoaderData<typeof loader>();
|
|
||||||
const fetcher = useFetcher();
|
const fetcher = useFetcher();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center">
|
<div className="flex h-screen items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-16">
|
<div className="flex flex-col items-center gap-16">
|
||||||
<header className="flex flex-col items-center gap-9">
|
<header className="flex flex-col items-center gap-9">
|
||||||
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
|
<img src="/bknd.svg" alt="bknd" className="block w-48 dark:invert" />
|
||||||
bknd w/ <span className="sr-only">Remix</span>
|
<div className="h-[144px] w-96">
|
||||||
</h1>
|
<img
|
||||||
<div className="h-[144px] w-[434px]">
|
src="/logo-light.svg"
|
||||||
<img src="/logo-light.png" alt="Remix" className="block w-full dark:hidden" />
|
alt="React Router"
|
||||||
<img src="/logo-dark.png" alt="Remix" className="hidden w-full dark:block" />
|
className="block w-full dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="React Router"
|
||||||
|
className="hidden w-full dark:block"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
||||||
@@ -136,9 +111,9 @@ export default function Index() {
|
|||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<a href="/admin">Go to Admin ➝</a>
|
<a href="/admin">Go to Admin ➝</a>
|
||||||
<div className="opacity-50 text-xs">
|
<div className="opacity-50 text-xs">
|
||||||
{ctx.user ? (
|
{user ? (
|
||||||
<p>
|
<p>
|
||||||
Authenticated as <b>{ctx.user.email}</b>
|
Authenticated as <b>{user.email}</b>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<a href="/admin/auth/login">Login</a>
|
<a href="/admin/auth/login">Login</a>
|
||||||
@@ -149,3 +124,39 @@ export default function Index() {
|
|||||||
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
examples/react-router/package.json
Normal file
33
examples/react-router/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "react-router",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "react-router build",
|
||||||
|
"dev": "react-router dev",
|
||||||
|
"start": "react-router-serve ./build/server/index.js",
|
||||||
|
"typecheck": "react-router typegen && tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-router/fs-routes": "^7.3.0",
|
||||||
|
"@react-router/node": "^7.3.0",
|
||||||
|
"@react-router/serve": "^7.3.0",
|
||||||
|
"bknd": "file:../../app",
|
||||||
|
"isbot": "^5.1.17",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router": "^7.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.3.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19.0.1",
|
||||||
|
"@types/react-dom": "^19.0.1",
|
||||||
|
"react-router-devtools": "^1.1.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^5.4.11",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
examples/react-router/public/bknd.svg
Normal file
14
examples/react-router/public/bknd.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
width="578"
|
||||||
|
height="188"
|
||||||
|
viewBox="0 0 578 188"
|
||||||
|
fill="black"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
|
||||||
|
/>
|
||||||
|
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
BIN
examples/react-router/public/favicon.ico
Normal file
BIN
examples/react-router/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
23
examples/react-router/public/logo-dark.svg
Normal file
23
examples/react-router/public/logo-dark.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.061 58.1641C71.1037 58.1641 58.1677 71.0742 58.1677 86.9996C58.1677 102.925 71.1037 115.835 87.061 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="white"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="white"/>
|
||||||
|
<path d="M289.314 144.671C289.314 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.314 160.596 289.314 144.671Z" fill="white"/>
|
||||||
|
<g clip-path="url(#clip0_202_2131)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.385 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.385 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="white"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="white"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="white"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="white"/>
|
||||||
|
<path d="M547.32 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.365 2.95282 554.365 13.1239C554.365 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.317 21.6426C553.595 22.8372 554.365 23.2391 554.365 30.0273V31.5345H547.332H547.32ZM522.457 18.3601H547.32V7.88763H522.457V18.349V18.3601Z" fill="white"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="white"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.746L631.335 31.5345H624.58L637.006 4.75034C637.71 3.22078 639.262 2.23828 640.936 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.283 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="white"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="white"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.147V7.02795H752.025V31.5345H745.282Z" fill="white"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.675 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_202_2131">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |
23
examples/react-router/public/logo-light.svg
Normal file
23
examples/react-router/public/logo-light.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
|
||||||
|
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.0608 58.1641C71.1035 58.1641 58.1676 71.0742 58.1676 86.9996C58.1676 102.925 71.1035 115.835 87.0608 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="#121212"/>
|
||||||
|
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="#121212"/>
|
||||||
|
<path d="M289.313 144.671C289.313 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.313 160.596 289.313 144.671Z" fill="#121212"/>
|
||||||
|
<g clip-path="url(#clip0_171_1761)">
|
||||||
|
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.386 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.386 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="#121212"/>
|
||||||
|
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="#121212"/>
|
||||||
|
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="#121212"/>
|
||||||
|
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="#121212"/>
|
||||||
|
<path d="M547.321 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.366 2.95282 554.366 13.1239C554.366 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.318 21.6426C553.595 22.8372 554.366 23.2391 554.366 30.0273V31.5345H547.332H547.321ZM522.457 18.3601H547.321V7.88763H522.457V18.349V18.3601Z" fill="#121212"/>
|
||||||
|
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="#121212"/>
|
||||||
|
<path d="M655.562 31.5345L653.151 26.3429H633.747L631.335 31.5345H624.58L637.007 4.75034C637.71 3.22078 639.262 2.23828 640.937 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.284 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="#121212"/>
|
||||||
|
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="#121212"/>
|
||||||
|
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.148V7.02795H752.026V31.5345H745.282Z" fill="#121212"/>
|
||||||
|
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.676 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="#121212"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_171_1761">
|
||||||
|
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.0 KiB |
7
examples/react-router/react-router.config.ts
Normal file
7
examples/react-router/react-router.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Config } from "@react-router/dev/config";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Config options...
|
||||||
|
// Server-side render by default, to enable SPA mode set this to `false`
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
27
examples/react-router/tsconfig.json
Normal file
27
examples/react-router/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"**/.server/**/*",
|
||||||
|
"**/.client/**/*",
|
||||||
|
".react-router/types/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./app/*"]
|
||||||
|
},
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
8
examples/react-router/vite.config.ts
Normal file
8
examples/react-router/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { reactRouter } from "@react-router/dev/vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||||
|
});
|
||||||
6
examples/remix/.gitignore
vendored
6
examples/remix/.gitignore
vendored
@@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
|
|
||||||
/.cache
|
|
||||||
/build
|
|
||||||
.env
|
|
||||||
*.db
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
|
||||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react";
|
|
||||||
import { ClientProvider } from "bknd/client";
|
|
||||||
import "./tailwind.css";
|
|
||||||
import { getApi } from "~/bknd";
|
|
||||||
|
|
||||||
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 const loader = async (args: LoaderFunctionArgs) => {
|
|
||||||
const api = await getApi(args);
|
|
||||||
return {
|
|
||||||
user: api.getUser()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const data = useLoaderData<typeof loader>();
|
|
||||||
return (
|
|
||||||
<ClientProvider user={data.user}>
|
|
||||||
<Outlet context={data} />
|
|
||||||
</ClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { adminPage } from "bknd/adapter/remix";
|
|
||||||
import "bknd/dist/styles.css";
|
|
||||||
|
|
||||||
export default adminPage({
|
|
||||||
config: {
|
|
||||||
basepath: "/admin",
|
|
||||||
logo_return_path: "/../",
|
|
||||||
theme: "system",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
@apply bg-white dark:bg-gray-950;
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +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.
|
Before Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"**/.server/**/*.ts",
|
|
||||||
"**/.server/**/*.tsx",
|
|
||||||
"**/.client/**/*.ts",
|
|
||||||
"**/.client/**/*.tsx"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
||||||
"types": ["@remix-run/node", "vite/client"],
|
|
||||||
"isolatedModules": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"target": "ES2022",
|
|
||||||
"strict": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["./app/*"]
|
|
||||||
},
|
|
||||||
"noEmit": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { vitePlugin as remix } from "@remix-run/dev";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
declare module "@remix-run/node" {
|
|
||||||
interface Future {
|
|
||||||
v3_singleFetch: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
tailwindcss(),
|
|
||||||
remix({
|
|
||||||
future: {
|
|
||||||
v3_fetcherPersist: true,
|
|
||||||
v3_relativeSplatPath: true,
|
|
||||||
v3_throwAbortReason: true,
|
|
||||||
v3_singleFetch: true,
|
|
||||||
v3_lazyRouteDiscovery: true
|
|
||||||
}
|
|
||||||
}) as any,
|
|
||||||
tsconfigPaths()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user