add: example for tanstack start

This commit is contained in:
2026-02-11 21:41:26 +05:30
parent 6288faef33
commit 01483b912f
29 changed files with 905 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import config from "../bknd.config";
import { getApp } from "bknd/adapter/tanstack-start";
export async function getApi({
headers,
verify,
}: {
verify?: boolean;
headers?: Headers;
}) {
const app = await getApp(config, process.env);
if (verify) {
const api = app.getApi({ headers });
await api.verifyAuth();
return api;
}
return app.getApi();
}

View File

@@ -0,0 +1,52 @@
import { useRouterState, Link } from "@tanstack/react-router";
export function Footer() {
const routerState = useRouterState();
const pathname = routerState.location.pathname;
return (
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
to={pathname === "/" ? "/ssr" : ("/" as string)}
>
<img
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
{pathname === "/" ? "SSR" : "Home"}
</Link>
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
to={"/admin" as string}
>
<img
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Admin
</Link>
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
to={"https://bknd.io" as string}
target="_blank"
rel="noopener noreferrer"
>
<img
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to bknd.io
</Link>
</footer>
);
}

View File

@@ -0,0 +1,9 @@
export const List = ({ items = [] }: { items: React.ReactNode[] }) => (
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
{items.map((item, i) => (
<li key={i} className={i < items.length - 1 ? "mb-2" : ""}>
{item}
</li>
))}
</ol>
);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,122 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as SsrRouteImport } from './routes/ssr'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiSplatRouteImport } from './routes/api.$'
import { Route as AdminSplatRouteImport } from './routes/admin.$'
const SsrRoute = SsrRouteImport.update({
id: '/ssr',
path: '/ssr',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSplatRoute = ApiSplatRouteImport.update({
id: '/api/$',
path: '/api/$',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSplatRoute = AdminSplatRouteImport.update({
id: '/admin/$',
path: '/admin/$',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/ssr': typeof SsrRoute
'/admin/$': typeof AdminSplatRoute
'/api/$': typeof ApiSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/ssr': typeof SsrRoute
'/admin/$': typeof AdminSplatRoute
'/api/$': typeof ApiSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/ssr': typeof SsrRoute
'/admin/$': typeof AdminSplatRoute
'/api/$': typeof ApiSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/ssr' | '/admin/$' | '/api/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/ssr' | '/admin/$' | '/api/$'
id: '__root__' | '/' | '/ssr' | '/admin/$' | '/api/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
SsrRoute: typeof SsrRoute
AdminSplatRoute: typeof AdminSplatRoute
ApiSplatRoute: typeof ApiSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/ssr': {
id: '/ssr'
path: '/ssr'
fullPath: '/ssr'
preLoaderRoute: typeof SsrRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/api/$': {
id: '/api/$'
path: '/api/$'
fullPath: '/api/$'
preLoaderRoute: typeof ApiSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/$': {
id: '/admin/$'
path: '/admin/$'
fullPath: '/admin/$'
preLoaderRoute: typeof AdminSplatRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SsrRoute: SsrRoute,
AdminSplatRoute: AdminSplatRoute,
ApiSplatRoute: ApiSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@@ -0,0 +1,17 @@
import { createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
export const getRouter = () => {
const router = createRouter({
routeTree,
context: {},
scrollRestoration: true,
defaultPreloadStaleTime: 0,
})
return router
}

View File

@@ -0,0 +1,58 @@
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanStackDevtools } from "@tanstack/react-devtools";
import { ClientProvider } from "bknd/client";
import appCss from "../styles.css?url";
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: "utf-8",
},
{
name: "viewport",
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack 🤝 Bknd.io",
},
],
links: [
{
rel: "stylesheet",
href: appCss,
},
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<ClientProvider verbose baseUrl={import.meta.env.APP_URL}>
{children}
</ClientProvider>
<TanStackDevtools
config={{
position: "bottom-right",
}}
plugins={[
{
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
<Scripts />
</body>
</html>
);
}

View File

@@ -0,0 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { useAuth } from "bknd/client";
import "bknd/dist/styles.css";
import { Admin } from "bknd/ui";
export const Route = createFileRoute("/admin/$")({
ssr: false,
component: RouteComponent,
});
function RouteComponent() {
const { user } = useAuth();
return (
<Admin
withProvider={{ user: user }}
config={{
basepath: "/admin",
logo_return_path: "/../",
}}
baseUrl={import.meta.env.APP_URL}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
import config from "../../bknd.config";
import { serve } from "bknd/adapter/tanstack-start";
const handler = serve(config);
export const Route = createFileRoute("/api/$")({
server: {
handlers: {
ANY: async ({ request }) => await handler(request),
},
},
});

View File

@@ -0,0 +1,171 @@
import {
createFileRoute,
useRouter,
} from "@tanstack/react-router";
import { getApi } from "@/bknd";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { Footer } from "@/components/Footer";
import { List } from "@/components/List";
export const completeTodo = createServerFn({ method: "POST" })
.inputValidator(
(data) => data as { done: boolean; id: number; title: string },
)
.handler(async ({ data: todo }) => {
try {
const api = await getApi({});
await api.data.updateOne("todos", todo.id, {
done: !todo.done,
});
console.log("state updated in db");
} catch (error) {
console.log(error);
}
});
export const deleteTodo = createServerFn({ method: "POST" })
.inputValidator((data) => data as { id: number })
.handler(async ({ data }) => {
try {
const api = await getApi({});
await api.data.deleteOne("todos", data.id);
console.log("todo deleted from db");
} catch (error) {
console.log(error);
}
});
export const createTodo = createServerFn({ method: "POST" })
.inputValidator((data) => data as { title: string })
.handler(async ({ data }) => {
try {
const api = await getApi({});
await api.data.createOne("todos", { title: data.title });
console.log("todo created in db");
} catch (error) {
console.log(error);
}
});
export const getTodo = createServerFn({ method: "POST" }).handler(async () => {
const api = await getApi({});
const limit = 5;
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
const total = todos.body.meta.total as number;
return { total, todos, limit };
});
export const Route = createFileRoute("/")({
ssr:false,
component: App,
loader: async () => {
return await getTodo();
},
});
function App() {
const { todos, total, limit } = Route.useLoaderData();
const router = useRouter();
const updateTodo = useServerFn(completeTodo);
const removeTodo = useServerFn(deleteTodo);
const addTodo = useServerFn(createTodo);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div className="flex flex-row items-center ">
<img
className="dark:invert size-18"
src="/tanstack-circle-logo.png"
alt="Next.js logo"
/>
<div className="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img
className="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width={183}
height={59}
/>
</div>
<Description />
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
<h2 className="font-mono mb-1 opacity-70">
<code>What's next?</code>
</h2>
<div className="flex flex-col w-full gap-2">
{total > limit && (
<div className="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
{total - limit} more todo(s) hidden
</div>
)}
<div className="flex flex-col gap-3">
{todos.reverse().map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
className="flex-shrink-0 cursor-pointer"
defaultChecked={!!todo.done}
onChange={async () => {
await updateTodo({ data: todo });
router.invalidate();
}}
/>
<div className="text-foreground/90 leading-none">
{todo.title}
</div>
</div>
<button
type="button"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
onClick={async () => {
await removeTodo({ data: { id: todo.id } });
router.invalidate();
}}
>
</button>
</div>
))}
</div>
<form
className="flex flex-row w-full gap-3 mt-2"
key={todos.map((t) => t.id).join()}
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const title = formData.get("title") as string;
await addTodo({ data: { title } });
router.invalidate();
e.currentTarget.reset();
}}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
/>
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
</div>
</div>
</main>
<Footer />
</div>
);
}
const Description = () => (
<List
items={[
"Get started with a full backend.",
"Focus on what matters instead of repetition.",
]}
/>
);

View File

@@ -0,0 +1,124 @@
import { getApi } from "@/bknd";
import { createServerFn } from "@tanstack/react-start";
import { Link } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { getRequest } from "@tanstack/react-start/server";
import { Footer } from "@/components/Footer";
import { List } from "@/components/List";
export const getTodo = createServerFn({ method: "POST" }).handler(async () => {
const api = await getApi({});
const limit = 5;
const todos = await api.data.readMany("todos");
const total = todos.body.meta.total as number;
return { total, todos, limit };
});
export const getUser = createServerFn({ method: "POST" }).handler(async () => {
const request = getRequest();
const api = await getApi({ verify: true, headers: request.headers });
const user = api.getUser();
return { user };
});
export const Route = createFileRoute("/ssr")({
component: RouteComponent,
loader: async () => {
return { ...(await getTodo()), ...(await getUser()) };
},
});
function RouteComponent() {
const { todos, user } = Route.useLoaderData();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div className="flex flex-row items-center ">
<img
className="dark:invert size-18"
src="/tanstack-circle-logo.png"
alt="Next.js logo"
/>
<div className="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img
className="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width={183}
height={59}
/>
</div>
<List items={todos.map((todo) => todo.title)} />
<Buttons />
<div>
{user ? (
<>
Logged in as {user.email}.{" "}
<Link
className="font-medium underline"
to={"/api/auth/logout" as string}
>
Logout
</Link>
</>
) : (
<div className="flex flex-col gap-1">
<p>
Not logged in.{" "}
<Link
className="font-medium underline"
to={"/admin/auth/login" as string}
>
Login
</Link>
</p>
<p className="text-xs opacity-50">
Sign in with:{" "}
<b>
<code>test@bknd.io</code>
</b>{" "}
/{" "}
<b>
<code>12345678</code>
</b>
</p>
</div>
)}
</div>
</main>
<Footer />
</div>
);
}
function Buttons() {
return (
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground gap-2 text-white hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://bknd.io/"
target="_blank"
rel="noopener noreferrer"
>
<img
className="grayscale"
src="/bknd.ico"
alt="bknd logomark"
width={20}
height={20}
/>
Go To Bknd.io
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://docs.bknd.io/integration/nextjs"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
);
}

View File

@@ -0,0 +1,25 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
@apply bg-background text-foreground;
font-family: Arial, Helvetica, sans-serif;
}