add: agnostic web compliant adapter
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 })}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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">&</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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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">&</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user