mirror of
https://github.com/shishantbiswas/bknd-examples.git
synced 2026-02-28 20:31:14 +00:00
init: test live deployment
This commit is contained in:
38
src/App.css
38
src/App.css
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Route = createRootRoute({
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
title: "TanStack Start Starter",
|
||||
title: "TanStack 🤝 Bknd.io",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">&</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
184
src/routes/ssr.tsx
Normal 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">&</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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user