init: test live deployment

This commit is contained in:
2026-02-10 01:37:15 +05:30
parent fc87ad1658
commit 1f1454b652
18 changed files with 878 additions and 216 deletions

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -9,10 +9,16 @@
// 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: '/',
@@ -31,36 +37,47 @@ const AdminSplatRoute = AdminSplatRouteImport.update({
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: '/' | '/admin/$' | '/api/$'
fullPaths: '/' | '/ssr' | '/admin/$' | '/api/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/admin/$' | '/api/$'
id: '__root__' | '/' | '/admin/$' | '/api/$'
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: '/'
@@ -87,6 +104,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SsrRoute: SsrRoute,
AdminSplatRoute: AdminSplatRoute,
ApiSplatRoute: ApiSplatRoute,
}

View File

@@ -16,7 +16,7 @@ export const Route = createRootRoute({
content: "width=device-width, initial-scale=1",
},
{
title: "TanStack Start Starter",
title: "TanStack 🤝 Bknd.io",
},
],
links: [

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import config from "../../bknd.config";
import { BkndConfig, createFrameworkApp } from "bknd/adapter";
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
// import { getApp } from "bknd/adapter/nextjs";
// const handler = serve({
@@ -10,13 +10,13 @@ import { BkndConfig, createFrameworkApp } from "bknd/adapter";
// --------------------------------- TANSTACK ADAPTER PROTOTYPE -----------------------------------
export async function getApp<Env = NodeJS.ProcessEnv>(
config: BkndConfig<Env>,
config: FrameworkBkndConfig<Env>,
args: Env = process.env as Env,
) {
return await createFrameworkApp(config, args);
}
function serve(config: BkndConfig) {
function serve(config: FrameworkBkndConfig) {
return async (request: Request) => {
const app = await getApp(config, process.env);
return app.fetch(request);

View File

@@ -1,142 +1,370 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import "../App.css";
import { useAuth } from "bknd/client";
import { useState } from "react";
import {
createFileRoute,
Link,
useRouter,
useRouterState,
} from "@tanstack/react-router";
import { getApi } from "@/bknd";
import { createServerFn, useServerFn } from "@tanstack/react-start";
export const Route = createFileRoute("/")({ component: App });
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 { user, verified, register, logout, login } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [mode, setMode] = useState<"login" | "register">("login");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { todos, total, limit } = Route.useLoaderData();
const router = useRouter();
async function handleSubmit(e: React.SubmitEvent) {
e.preventDefault();
setLoading(true);
setError(null);
try {
if (mode === "login") {
// attempt login
await login({ email, password } as any);
} else {
// attempt register
await register({ email, password } as any);
}
setEmail("");
setPassword("");
} catch (err: any) {
setError(err?.message ?? String(err));
} finally {
setLoading(false);
}
}
async function handleLogout() {
setLoading(true);
try {
await logout();
} catch (err: any) {
setError(err?.message ?? String(err));
} finally {
setLoading(false);
}
}
const updateTodo = useServerFn(completeTodo);
const removeTodo = useServerFn(deleteTodo);
const addTodo = useServerFn(createTodo);
return (
<div>
<main style={{ padding: 20 }} className="App-header">
<header>
<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="TanStack Logo"
style={{ width: "100px", height: "100px" }}
alt="Next.js logo"
/>
</header>
<section>
</section>
<section style={{ maxWidth: 420, margin: "0 auto" }}>
<h2 style={{margin:"24px 0 "}}>Account</h2>
{user ? (
<div style={{ gap: 8, display: "flex", flexDirection: "column" }}>
<div>
<strong>Signed in as:</strong> {user?.email ?? "Unknown"}
</div>
<div>
<strong>Verified:</strong> {verified ? "Yes" : "No"}
</div>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={handleLogout} disabled={loading}>
{loading ? "Signing out..." : "Sign out"}
</button>
<Link to={"/admin" as string}>
<button>Go to Admin</button>
</Link>
<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 onSubmit={handleSubmit} style={{ display: "grid", gap: 8 }}>
<div style={{ display: "flex", gap: 8 }}>
<button
type="button"
onClick={() => setMode("login")}
style={{ textDecoration: mode === "login" ? "underline" : "none" }}
>
Log in
</button>
<button
type="button"
onClick={() => setMode("register")}
style={{ textDecoration: mode === "register" ? "underline" : "none" }}
>
Register
</button>
</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
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{
border: "1px solid white",
padding: "4px",
borderRadius: "4px"
}}
type="email"
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"
/>
<input
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={{
border: "1px solid white",
padding: "4px",
borderRadius: "4px"
}}
type="password"
/>
{error ? <div style={{ color: "#ff6b6b" }}>{error}</div> : null}
<button type="submit" disabled={loading}>
{loading
? "Please wait..."
: mode === "login"
? "Log in"
: "Create account"}
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
)}
</section>
</div>
</div>
</main>
<Footer />
</div>
);
}
const Description = () => (
<List
items={[
"Get started with a full backend.",
"Focus on what matters instead of repetition.",
]}
/>
);
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>
);
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>
);
}
// function App() {
// const { user, verified, register, logout, login } = useAuth();
// const [email, setEmail] = useState("");
// const [password, setPassword] = useState("");
// const [mode, setMode] = useState<"login" | "register">("login");
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState<string | null>(null);
// async function handleSubmit(e: React.SubmitEvent) {
// e.preventDefault();
// setLoading(true);
// setError(null);
// try {
// if (mode === "login") {
// // attempt login
// await login({ email, password } as any);
// } else {
// // attempt register
// await register({ email, password } as any);
// }
// setEmail("");
// setPassword("");
// } catch (err: any) {
// setError(err?.message ?? String(err));
// } finally {
// setLoading(false);
// }
// }
// async function handleLogout() {
// setLoading(true);
// try {
// await logout();
// } catch (err: any) {
// setError(err?.message ?? String(err));
// } finally {
// setLoading(false);
// }
// }
// return (
// <div>
// <main style={{ padding: 20 }} className="App-header">
// <header>
// <img
// src="/tanstack-circle-logo.png"
// alt="TanStack Logo"
// style={{ width: "100px", height: "100px" }}
// />
// </header>
// <section></section>
// <section style={{ maxWidth: 420, margin: "0 auto" }}>
// <h2 style={{ margin: "24px 0 " }}>Account</h2>
// {user ? (
// <div style={{ gap: 8, display: "flex", flexDirection: "column" }}>
// <div>
// <strong>Signed in as:</strong> {user?.email ?? "Unknown"}
// </div>
// <div>
// <strong>Verified:</strong> {verified ? "Yes" : "No"}
// </div>
// <div style={{ display: "flex", gap: 8 }}>
// <button onClick={handleLogout} disabled={loading}>
// {loading ? "Signing out..." : "Sign out"}
// </button>
// <Link to={"/admin" as string}>
// <button>Go to Admin</button>
// </Link>
// </div>
// </div>
// ) : (
// <form onSubmit={handleSubmit} style={{ display: "grid", gap: 8 }}>
// <div style={{ display: "flex", gap: 8 }}>
// <button
// type="button"
// onClick={() => setMode("login")}
// style={{
// textDecoration: mode === "login" ? "underline" : "none",
// }}
// >
// Log in
// </button>
// <button
// type="button"
// onClick={() => setMode("register")}
// style={{
// textDecoration: mode === "register" ? "underline" : "none",
// }}
// >
// Register
// </button>
// </div>
// <input
// placeholder="Email"
// value={email}
// onChange={(e) => setEmail(e.target.value)}
// required
// style={{
// border: "1px solid white",
// padding: "4px",
// borderRadius: "4px",
// }}
// type="email"
// />
// <input
// placeholder="Password"
// value={password}
// onChange={(e) => setPassword(e.target.value)}
// required
// style={{
// border: "1px solid white",
// padding: "4px",
// borderRadius: "4px",
// }}
// type="password"
// />
// {error ? <div style={{ color: "#ff6b6b" }}>{error}</div> : null}
// <button type="submit" disabled={loading}>
// {loading
// ? "Please wait..."
// : mode === "login"
// ? "Log in"
// : "Create account"}
// </button>
// </form>
// )}
// </section>
// </main>
// </div>
// );
// }

184
src/routes/ssr.tsx Normal file
View File

@@ -0,0 +1,184 @@
import { createFileRoute, useRouterState } from "@tanstack/react-router";
import { getRequest } from "@tanstack/react-start/server";
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()) };
},
});
import { getApi } from "@/bknd";
import { createServerFn } from "@tanstack/react-start";
import { Link } from "@tanstack/react-router";
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>
);
}
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>
);
}
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>
);
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

@@ -1,14 +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 {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
@apply bg-background text-foreground;
font-family: Arial, Helvetica, sans-serif;
}