add: agnostic web compliant adapter

This commit is contained in:
2026-04-04 00:27:29 +05:30
parent 8a90b9356b
commit 54aae56734
41 changed files with 1534 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
import { Link } from "@builder.io/qwik-city";
import { component$ } from "@builder.io/qwik";
import { useLocation } from "@builder.io/qwik-city";
export default component$(() => {
const loc = useLocation();
return (
<footer class="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<Link
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
href={loc.url.pathname === '/' ? '/user' : '/'}
>
<img aria-hidden src="/file.svg" alt="File icon" width="16" height="16" />
{loc.url.pathname === "/" ? "User" : "Home"}
</Link>
<Link
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/admin/data"
>
<img aria-hidden src="/window.svg" alt="Window icon" width="16" height="16" />
Admin
</Link>
<a
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://bknd.io"
target="_blank"
rel="noopener noreferrer"
>
<img aria-hidden src="/globe.svg" alt="Globe icon" width="16" height="16" />
Go to bknd.io →
</a>
</footer>
);
});
+17
View File
@@ -0,0 +1,17 @@
import { component$ } from "@builder.io/qwik";
interface Props {
items: string[];
}
export default component$<Props>(({ items }) => {
return (
<ol class="list-inside list-decimal text-sm text-center sm:text-left w-full text-center">
{items.map((item, i) => (
<li key={i} class={`${i < items.length - 1 ? "mb-2" : ""}`}>
<span>{item}</span>
</li>
))}
</ol>
);
});
+17
View File
@@ -0,0 +1,17 @@
/*
* WHAT IS THIS FILE?
*
* Development entry point using only client-side modules:
* - Do not use this mode in production!
* - No SSR
* - No portion of the application is pre-rendered on the server.
* - All of the application is running eagerly in the browser.
* - More code is transferred to the browser than in SSR mode.
* - Optimizer/Serialization/Deserialization code is not exercised!
*/
import { render, type RenderOptions } from "@builder.io/qwik";
import Root from "./root";
export default function (opts: RenderOptions) {
return render(document, <Root />, opts);
}
+21
View File
@@ -0,0 +1,21 @@
/*
* WHAT IS THIS FILE?
*
* It's the bundle entry point for `npm run preview`.
* That is, serving your app built in production mode.
*
* Feel free to modify this file, but don't remove it!
*
* Learn more about Vite's preview command:
* - https://vitejs.dev/config/preview-options.html#preview-options
*
*/
import { createQwikCity } from "@builder.io/qwik-city/middleware/node";
import qwikCityPlan from "@qwik-city-plan";
// make sure qwikCityPlan is imported before entry
import render from "./entry.ssr";
/**
* The default export is the QwikCity adapter used by Vite preview.
*/
export default createQwikCity({ render, qwikCityPlan });
+31
View File
@@ -0,0 +1,31 @@
/**
* WHAT IS THIS FILE?
*
* SSR entry point, in all cases the application is rendered outside the browser, this
* entry point will be the common one.
*
* - Server (express, cloudflare...)
* - npm run start
* - npm run preview
* - npm run build
*
*/
import {
renderToStream,
type RenderToStreamOptions,
} from "@builder.io/qwik/server";
import Root from "./root";
export default function (opts: RenderToStreamOptions) {
return renderToStream(<Root />, {
...opts,
// Use container attributes to set attributes on the html tag.
containerAttributes: {
lang: "en-us",
...opts.containerAttributes,
},
serverData: {
...opts.serverData,
},
});
}
+23
View File
@@ -0,0 +1,23 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
@apply bg-background text-foreground;
font-family: Arial, Helvetica, sans-serif;
}
+13
View File
@@ -0,0 +1,13 @@
import { createBknd } from "bknd/adapter/web";
import bkndConfig from "../../bknd.config";
import type { EnvGetter } from "@builder.io/qwik-city/middleware/request-handler";
export const getApp = async (env?: EnvGetter) => createBknd({ mode: "standalone", options: bkndConfig }, env);
export const handler = async (req: Request, env?: EnvGetter) => {
return (await getApp(env)).serve()(req);
};
export const getApi = async (opts?: { headers?: Headers; verify?: boolean }) => {
return (await getApp()).getApi(opts);
};
+77
View File
@@ -0,0 +1,77 @@
import { component$, isDev } from "@builder.io/qwik";
import { QwikCityProvider, RouterOutlet } from "@builder.io/qwik-city";
import { useDocumentHead, useLocation } from "@builder.io/qwik-city";
import "./global.css";
export default component$(() => {
/**
* The root of a QwikCity site always start with the <QwikCityProvider> component,
* immediately followed by the document's <head> and <body>.
*
* Don't remove the `<head>` and `<body>` elements.
*/
return (
<QwikCityProvider>
<head>
<meta charset="utf-8" />
{!isDev && (
<link
rel="manifest"
href={`${import.meta.env.BASE_URL}manifest.json`}
/>
)}
<RouterHead />
</head>
<body lang="en">
<RouterOutlet />
</body>
</QwikCityProvider>
);
});
export const RouterHead = component$(() => {
const head = useDocumentHead();
const loc = useLocation();
return (
<>
<title>{head.title}</title>
<link rel="canonical" href={loc.url.href} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
{head.meta.map((m) => (
<meta key={m.key} {...m} />
))}
{head.links.map((l) => (
<link key={l.key} {...l} />
))}
{head.styles.map((s) => (
<style
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.style })}
/>
))}
{head.scripts.map((s) => (
<script
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML
? {}
: { dangerouslySetInnerHTML: s.script })}
/>
))}
</>
);
});
+132
View File
@@ -0,0 +1,132 @@
import { component$ } from "@builder.io/qwik";
import { Form, routeAction$, routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
import Footer from "~/components/footer";
import List from "~/components/list";
import { getApi } from "~/lib/bknd";
interface Todo {
title: string | undefined;
done: boolean | undefined;
id: number;
}
export const useGetTodos = routeLoader$(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 useCompleteTodo = routeAction$(async (data) => {
const api = await getApi();
const todo = data as unknown as Todo;
await api.data.updateOne("todos", todo.id, {
done: !todo.done,
});
});
export const useDeleteTodo = routeAction$(async (data) => {
const api = await getApi();
const todo = data as unknown as Todo;
await api.data.deleteOne("todos", todo.id);
});
export const useAddTodo = routeAction$(async (data) => {
const api = await getApi();
await api.data.createOne("todos", { title: data.title as string });
});
export default component$(() => {
const {
value: { limit, todos, total },
} = useGetTodos();
const addTodoAction = useAddTodo();
const completeTodoAction = useCompleteTodo();
const deleteTodoAction = useDeleteTodo();
return (
<div class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main class="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div class="flex flex-row items-center justify-evenly min-w-full">
<img class="size-24" src="/favicon.svg" alt="Qwik logo" width="72" height="72" />
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img class="dark:invert" src="/bknd.svg" alt="bknd logo" width="183" height="59" />
</div>
<List
items={[
"Get started with a full backend.",
"Focus on what matters instead of repetition.",
]}
/>
<div class="flex flex-col border border-black/15 dark:border-white/15 w-full py-4 px-5 gap-2">
<h2 class="font-mono mb-1 opacity-70">
<code>What's next?</code>
</h2>
<div class="flex flex-col w-full gap-2">
{total > limit && (
<div class="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
{total - limit} more todo(s) hidden
</div>
)}
<div class="flex flex-col gap-3">
{todos.map((todo) => (
<div key={String(todo.id)} class="flex flex-row">
<div class="flex flex-row grow items-center gap-3 ml-1">
<input
type="checkbox"
class="shrink-0 cursor-pointer"
checked={Boolean(todo.done)}
onChange$={async() => {
await completeTodoAction.submit(todo);
}}
/>
<div class="text-foreground/90 leading-none">{todo.title}</div>
</div>
<button
type="button"
class="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs"
onclick$={async () => { await deleteTodoAction.submit(todo); }}
>
❌
</button>
</div>
))}
</div>
<Form
class="flex flex-row w-full gap-3 mt-2"
key={todos.map((t) => t.id).join()}
action={addTodoAction}
>
<input
type="text"
name="title"
placeholder="New todo"
class="py-2 px-4 flex grow rounded-sm bg-black/5 focus:bg-black/10 dark:bg-white/5 dark:focus:bg-white/10 transition-colors outline-none"
/>
<button type="submit" class="cursor-pointer">
Add
</button>
</Form>
</div>
</div>
</main>
<Footer />
</div>
);
});
export const head: DocumentHead = {
title: "Qwik City 🤝 Bknd.io",
meta: [
{
name: "description",
content: "Qwik site with bknd",
},
],
};
+45
View File
@@ -0,0 +1,45 @@
import type { RequestHandler } from "@builder.io/qwik-city";
import { handler } from "~/lib/bknd";
export const onRequest: RequestHandler = async ({
env,
url,
next,
status,
headers,
request,
redirect,
getWritableStream,
}) => {
const pathname = url.pathname;
if (pathname.startsWith("/api") || pathname !== "/") {
const response = await handler(request, env);
// skips unknown paths
if (response.status === 404) {
await next();
return;
}
// adds the set-cookie header
response.headers.forEach((value, key) => {
headers.set(key, value);
});
// for redirect
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get("location");
if (location) {
throw redirect(response.status as any, location);
}
}
// stream back the body
status(response.status);
if (response.body) {
await response.body?.pipeTo(getWritableStream());
return;
}
}
};
+95
View File
@@ -0,0 +1,95 @@
import { component$ } from "@builder.io/qwik";
import { Link, routeLoader$ } from "@builder.io/qwik-city";
import List from "~/components/list";
import { getApi } from "~/lib/bknd";
import Footer from "~/components/footer";
export const useGetTodos = routeLoader$(async ({ request }) => {
const api = await getApi({ verify: true, headers: request.headers });
const limit = 5;
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
const total = todos.body.meta.total as number;
const user = api.getUser();
return { total, todos, limit, user };
});
export default component$(() => {
const {
value: { todos, user },
} = useGetTodos();
return (
<div class="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main class="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div class="flex flex-row items-center ">
<img
class="dark:invert size-18"
src="/favicon.svg"
alt="Qwik logo"
height={72}
width={72}
/>
<div class="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<img class="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 class="font-medium underline" href={"/api/auth/logout" as string}>
Logout
</Link>
</>
) : (
<div class="flex flex-col gap-1">
<p>
Not logged in.
<Link class="font-medium underline" href={"/admin/auth/login" as string}>
Login
</Link>
</p>
<p class="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 class="flex gap-4 items-center flex-col sm:flex-row">
<a
class="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 class="grayscale" src="/bknd.ico" alt="bknd logomark" width={20} height={20} />
Go To Bknd.io
</a>
<a
class="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/meta-framework"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
);
}