init: solid start adapter

This commit is contained in:
2026-03-28 16:21:42 +05:30
parent 81bf41ce34
commit 8d37f9df3f
34 changed files with 1099 additions and 2 deletions
+19
View 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
View 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">&amp;</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
View 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">&amp;</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>
);
}