mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
init: solid start adapter
This commit is contained in:
33
examples/solid/src/app.css
Normal file
33
examples/solid/src/app.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
.geist-mono-100 {
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
font-family: "Geist Mono", monospace;
|
||||
}
|
||||
|
||||
@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: var(--font-geist-mono-100)
|
||||
}
|
||||
20
examples/solid/src/app.tsx
Normal file
20
examples/solid/src/app.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MetaProvider, Title } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { Suspense } from "solid-js";
|
||||
import "./app.css";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
root={props => (
|
||||
<MetaProvider>
|
||||
<Title>Solid Start 🤝 Bknd.io</Title>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
53
examples/solid/src/components/Footer.tsx
Normal file
53
examples/solid/src/components/Footer.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { A, useLocation } from "@solidjs/router";
|
||||
|
||||
export function Footer() {
|
||||
const pathname = useLocation().pathname;
|
||||
|
||||
return (
|
||||
<footer class="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<A
|
||||
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href={pathname === "/" ? "/user" : "/"}
|
||||
>
|
||||
<img
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{pathname === "/" ? "User" : "Home"}
|
||||
</A>
|
||||
<A
|
||||
target="_self"
|
||||
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="/admin"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Admin
|
||||
</A>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
11
examples/solid/src/components/List.tsx
Normal file
11
examples/solid/src/components/List.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
export const List = ({ items = [] }: { items: JSX.Element[] }) => (
|
||||
<ol class="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
{items.map((item, i) => (
|
||||
<li class={i < items.length - 1 ? "mb-2" : ""}>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
4
examples/solid/src/entry-client.tsx
Normal file
4
examples/solid/src/entry-client.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
// @refresh reload
|
||||
import { mount, StartClient } from "@solidjs/start/client";
|
||||
|
||||
mount(() => <StartClient />, document.getElementById("app")!);
|
||||
21
examples/solid/src/entry-server.tsx
Normal file
21
examples/solid/src/entry-server.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// @refresh reload
|
||||
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||
|
||||
export default createHandler(() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/solid.svg" />
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">{children}</div>
|
||||
{scripts}
|
||||
</body>
|
||||
</html>
|
||||
)}
|
||||
/>
|
||||
));
|
||||
1
examples/solid/src/global.d.ts
vendored
Normal file
1
examples/solid/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@solidjs/start/env" />
|
||||
30
examples/solid/src/lib/bknd.ts
Normal file
30
examples/solid/src/lib/bknd.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getApp as getBkndApp } from "bknd/adapter/solid-start";
|
||||
import bkndConfig from "../../bknd.config";
|
||||
import type { App } from "bknd";
|
||||
|
||||
let client: App | null = null;
|
||||
|
||||
export const getApp = async () => {
|
||||
if (!client) {
|
||||
client = await getBkndApp(bkndConfig);
|
||||
}
|
||||
return client;
|
||||
};
|
||||
|
||||
export async function getApi({
|
||||
headers,
|
||||
verify,
|
||||
}: {
|
||||
verify?: boolean;
|
||||
headers?: Headers;
|
||||
}) {
|
||||
const app = await getApp();
|
||||
|
||||
if (verify) {
|
||||
const api = app.getApi({ headers });
|
||||
await api.verifyAuth();
|
||||
return api;
|
||||
}
|
||||
|
||||
return app.getApi();
|
||||
}
|
||||
20
examples/solid/src/middleware/index.ts
Normal file
20
examples/solid/src/middleware/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createMiddleware } from "@solidjs/start/middleware";
|
||||
import config from "../../bknd.config";
|
||||
import { serve } from "bknd/adapter/solid-start";
|
||||
|
||||
const handler = serve(config);
|
||||
|
||||
export default createMiddleware({
|
||||
onRequest: async (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname.startsWith("/api") || pathname !== "/") {
|
||||
const res = await handler(event.request);
|
||||
|
||||
if (res && res.status !== 404) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
19
examples/solid/src/routes/[...404].tsx
Normal file
19
examples/solid/src/routes/[...404].tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { HttpStatusCode } from "@solidjs/start";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<h1>Page Not Found</h1>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build SolidStart apps.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
150
examples/solid/src/routes/index.tsx
Normal file
150
examples/solid/src/routes/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { DB } from "bknd";
|
||||
import { Suspense } from "solid-js";
|
||||
import { Footer } from "~/components/Footer";
|
||||
import { List } from "~/components/List";
|
||||
import { getApi } from "~/lib/bknd";
|
||||
import { action, redirect, useAction, useSubmission } from "@solidjs/router";
|
||||
import { query, createAsync } from "@solidjs/router";
|
||||
|
||||
type Todo = DB['todos'];
|
||||
|
||||
export const getTodo = async () => {
|
||||
"use server"
|
||||
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: todos as unknown as Todo[], limit };
|
||||
};
|
||||
|
||||
const getTodosFromServer = query(async () => await getTodo(), "getTodosFromServer");
|
||||
|
||||
|
||||
const createTodo = action(async (formData: FormData) => {
|
||||
"use server"
|
||||
const title = formData.get("title") as string;
|
||||
const api = await getApi({});
|
||||
await api.data.createOne("todos", { title });
|
||||
throw redirect("/", { revalidate: getTodosFromServer.keyFor() });
|
||||
}, "createTodo");
|
||||
|
||||
|
||||
|
||||
const completeTodo = action(async (todo: Todo) => {
|
||||
"use server"
|
||||
const api = await getApi({});
|
||||
await api.data.updateOne("todos", todo.id, {
|
||||
done: !todo.done,
|
||||
});
|
||||
throw redirect("/", { revalidate: getTodosFromServer.keyFor() });
|
||||
}, "completeTodo");
|
||||
|
||||
|
||||
const deleteTodo = action(async (todo: Todo) => {
|
||||
"use server"
|
||||
const api = await getApi({});
|
||||
await api.data.deleteOne("todos", todo.id);
|
||||
throw redirect("/", { revalidate: getTodosFromServer.keyFor() });
|
||||
}, "deleteTodo");
|
||||
|
||||
export default function Home() {
|
||||
const data = createAsync(() => getTodosFromServer());
|
||||
|
||||
const submission = useSubmission(createTodo);
|
||||
|
||||
const updateTodo = useAction(completeTodo);
|
||||
const removeTodo = useAction(deleteTodo);
|
||||
|
||||
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="/solid.svg"
|
||||
alt="Solid Start logo"
|
||||
/>
|
||||
<div class="ml-3.5 mr-2 opacity-70">&</div>
|
||||
<img
|
||||
class="dark:invert"
|
||||
src="/bknd.svg"
|
||||
alt="bknd logo"
|
||||
width={183}
|
||||
height={59}
|
||||
/>
|
||||
</div>
|
||||
<Description />
|
||||
<Suspense>
|
||||
<div class="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
|
||||
<h2 class=" mb-1 opacity-70">
|
||||
<code>What's next?</code>
|
||||
</h2>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
{(data()?.total ?? 0) > (data()?.limit ?? 0) && (
|
||||
<div class="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
|
||||
{(data()?.total ?? 0) - (data()?.limit ?? 0)} more todo(s) hidden
|
||||
</div>
|
||||
)}
|
||||
<div class="flex flex-col gap-3">
|
||||
{data()?.todos?.
|
||||
splice(0, data()?.limit ?? 0)
|
||||
.map((todo) => (
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row flex-grow items-center gap-3 ml-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="flex-shrink-0 cursor-pointer"
|
||||
checked={!!todo.done}
|
||||
onChange={async () => {
|
||||
await updateTodo(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 removeTodo(todo);
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<form
|
||||
class="flex flex-row w-full gap-3 mt-2"
|
||||
action={createTodo}
|
||||
method="post"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="New todo"
|
||||
class="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
|
||||
/>
|
||||
<button type="submit" class="cursor-pointer" disabled={submission.pending}>
|
||||
{submission.pending ? "Adding..." : "Add"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Description = () => (
|
||||
<List
|
||||
items={[
|
||||
"Get started with a full backend.",
|
||||
"Focus on what matters instead of repetition.",
|
||||
]}
|
||||
/>
|
||||
);
|
||||
139
examples/solid/src/routes/user.tsx
Normal file
139
examples/solid/src/routes/user.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import type { DB } from "bknd";
|
||||
import { createResource, Suspense } from "solid-js";
|
||||
import { getRequestEvent } from "solid-js/web";
|
||||
import { Footer } from "~/components/Footer";
|
||||
import { List } from "~/components/List";
|
||||
import { getApi } from "~/lib/bknd";
|
||||
|
||||
|
||||
const getUser = async () => {
|
||||
"use server"
|
||||
const request = getRequestEvent()?.request;
|
||||
const api = await getApi({ verify: true, headers: request?.headers });
|
||||
return api.getUser();
|
||||
}
|
||||
|
||||
type Todo = DB['todos'];
|
||||
export const getTodo = async () => {
|
||||
"use server"
|
||||
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: todos as unknown as Todo[], limit };
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [data] = createResource(async () => {
|
||||
const todo = await getTodo();
|
||||
const user = await getUser()
|
||||
return { todo, user };
|
||||
}, {
|
||||
initialValue: {
|
||||
todo: {
|
||||
todos: [],
|
||||
limit: 0,
|
||||
total: 0
|
||||
},
|
||||
user: null
|
||||
}
|
||||
});
|
||||
|
||||
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="/solid.svg"
|
||||
alt="Solid logo"
|
||||
/>
|
||||
<div class="ml-3.5 mr-2 opacity-70">&</div>
|
||||
<img
|
||||
class="dark:invert"
|
||||
src="/bknd.svg"
|
||||
alt="bknd logo"
|
||||
width={183}
|
||||
height={59}
|
||||
/>
|
||||
</div>
|
||||
<List items={(data()?.todo.todos ?? []).map((todo) => todo.title)} />
|
||||
<Buttons />
|
||||
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<div>
|
||||
{data()?.user ? (
|
||||
<>
|
||||
Logged in as {data()?.user?.email}.
|
||||
<A
|
||||
class="underline"
|
||||
// target="_self" is required to prevent solid router from intercepting the link
|
||||
target="_self"
|
||||
href={"/api/auth/logout"}
|
||||
>
|
||||
Logout
|
||||
</A>
|
||||
</>
|
||||
) : (
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>
|
||||
Not logged in.
|
||||
<A
|
||||
class="underline"
|
||||
// target="_self" is required to prevent solid router from intercepting the link
|
||||
target="_self"
|
||||
href={"/admin/auth/login"}
|
||||
>
|
||||
Login
|
||||
</A>
|
||||
</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>
|
||||
</Suspense>
|
||||
</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/start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user