todo app done

main
Ethan Niser 2023-09-13 23:44:22 -05:00
parent de5a74e593
commit 65c49c7280
10 changed files with 223 additions and 59 deletions

@ -3,8 +3,8 @@
"module": "src/main.ts",
"type": "module",
"scripts": {
"dev": "bun run --hot src/main.ts",
"start": "bun run src/main.ts",
"dev": "bun run --hot src/main.tsx",
"start": "bun run src/main.tsx",
"db:push": "bunx drizzle-kit push:sqlite",
"db:studio": "bunx drizzle-kit studio",
"db:seed": "bun run src/model/store/seed.ts"

@ -19,7 +19,14 @@ export const ctx = new Elysia({
})
)
.decorate("db", db)
.decorate("config", config);
.decorate("config", config)
.decorate(
"html",
(html: string) =>
new Response(html, {
headers: { "Content-Type": "text/html; charset=utf8" },
})
);
// .onStart(({ log }) => log.info("Server starting"))
// .onStop(({ log }) => log.info("Server stopping"))
// .onRequest(({ log, request }) => {

@ -1,9 +1,10 @@
import Elysia from "elysia";
import Elysia, { t } from "elysia";
import { ctx } from "../context";
import { insertTodoSchema } from "../model/todo";
import { TodoItem } from "../views/todoItem";
import { insertTodoSchema, todos } from "../model/todo";
import { TodoItem, TodoForm, TodoList } from "../views/todos";
import Html from "@kitajs/html";
import { db as works } from "../model/store";
import { db } from "../model/store";
import { eq } from "drizzle-orm";
export const todosController = new Elysia({
name: "@app/todos",
@ -13,16 +14,80 @@ export const todosController = new Elysia({
.model({
todo: insertTodoSchema,
})
.get("/", async ({ log, db }) => {
console.log("db", db === works);
log.info("get todos");
const todos = await works.query.todos.findMany();
return (
<div hx-get="/todos">
{todos.map((todo) => (
<TodoItem {...todo} />
))}
</div>
);
});
.get("/", async () => {
const data = await db.select().from(todos).limit(10);
return <TodoList todos={data} />;
})
.post(
"/toggle/:id",
async ({ params }) => {
const [oldTodo] = await db
.select()
.from(todos)
.where(eq(todos.id, params.id));
if (!oldTodo) {
throw new Error("Todo not found");
}
const [newTodo] = await db
.update(todos)
.set({ completed: !oldTodo.completed })
.where(eq(todos.id, params.id))
.returning();
if (!newTodo) {
throw new Error("Todo not found");
}
return <TodoItem {...newTodo} />;
},
{
params: t.Object({
id: t.Numeric(),
}),
}
)
.delete(
"/:id",
async ({ params }) => {
await db.delete(todos).where(eq(todos.id, params.id));
},
{
params: t.Object({
id: t.Numeric(),
}),
}
)
.post(
"",
async ({ body }) => {
const content = {
beth: "Learn the BETH stack",
vim: "Learn vim",
like: "Like the video",
sub: "Subscribe to Ethan",
};
const [newTodo] = await db
.insert(todos)
.values({ content: content[body.content] })
.returning();
if (!newTodo) {
throw new Error("Todo not found");
}
return <TodoItem {...newTodo} />;
},
{
body: t.Object({
content: t.Union([
t.Literal("beth"),
t.Literal("vim"),
t.Literal("like"),
t.Literal("sub"),
]),
}),
}
);

29
src/htmx.d.ts vendored

@ -1,15 +1,30 @@
type RoutesByType<
Schema extends Record<string, unknown>,
Type extends "get" | "post" | "put" | "delete" | "patch"
> = RemoveSlash<
keyof {
[key in keyof Schema as Schema[key] extends { [key in Type]: unknown }
? key
: never]: true;
}
> = RouterPattern<
RemoveSlash<
keyof {
[key in keyof Schema as Schema[key] extends { [key in Type]: unknown }
? key
: never]: true;
}
>
>;
type RemoveSlash<S extends string> = S extends `${infer T}/` ? T : S;
type RemoveSlash<S extends string> = S extends `${infer T}/`
? T extends ""
? S
: T
: S;
type RouterPattern<T extends string> =
T extends `${infer Start}:${infer Param}/${infer Rest}`
? `${Start}${string}/${RouterPattern<Rest>}`
: T extends `${infer Start}:${infer Param}`
? `${Start}${string}`
: T extends `${infer Start}*`
? `${Start}${string}`
: T;
declare namespace JSX {
type Schema = import("./main").App["meta"]["schema"];

@ -1,21 +0,0 @@
import { Elysia } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { staticPlugin } from "@elysiajs/static";
import { todosController } from "./controllers/todos";
const app = new Elysia({
name: "@app/main",
})
// .use(swagger())
.use(staticPlugin())
.use(todosController)
.listen(3000);
export type App = typeof app;
console.log(`app is listening on ${app.server?.hostname}:${app.server?.port}`);
app
.handle(new Request("http://localhost:3000/todos"))
.then((res) => res.text())
.then(console.log);

@ -0,0 +1,32 @@
import { Elysia } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { staticPlugin } from "@elysiajs/static";
import { todosController } from "./controllers/todos";
import { BaseHtml } from "./views/base";
import Html from "@kitajs/html";
const app = new Elysia({
name: "@app/main",
})
// .use(swagger())
// .use(staticPlugin())
.use(todosController)
.get("/", ({ html }) =>
html(
<BaseHtml>
<body
class="flex w-full h-screen justify-center items-center"
hx-get="/todos"
hx-swap="innerHTML"
hx-trigger="load"
/>
</BaseHtml>
)
)
.listen(3000);
export type App = typeof app;
console.log(
`app is listening on http://${app.server?.hostname}:${app.server?.port}`
);

@ -0,0 +1,18 @@
import Html from "@kitajs/html";
export const BaseHtml = ({ children }: Html.PropsWithChildren) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>THE BETH STACK</title>
<script src="https://unpkg.com/htmx.org@1.9.3"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.9"></script>
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css">
</head>
${children}
`;

@ -1,11 +0,0 @@
import type { Todo } from "../model/todo";
import Html from "@kitajs/html";
export const TodoItem = (todo: Todo) => {
return (
<li>
<input type="checkbox" checked={todo.completed} />
<span>{todo.content}</span>
</li>
);
};

@ -0,0 +1,58 @@
import type { Todo } from "../model/todo";
import Html from "@kitajs/html";
export function TodoItem({ content, completed, id }: Todo) {
return (
<div class="flex flex-row space-x-3">
<p>{content}</p>
<input
type="checkbox"
checked={completed}
hx-post={`/todos/toggle/${id}`}
hx-swap="outerHTML"
hx-target="closest div"
/>
<button
class="text-red-500"
hx-delete={`/todos/${id}`}
hx-swap="outerHTML"
hx-target="closest div"
>
X
</button>
</div>
);
}
export function TodoList({ todos }: { todos: Todo[] }) {
return (
<div>
{todos.map((todo) => (
<TodoItem {...todo} />
))}
<TodoForm />
</div>
);
}
export function TodoForm() {
return (
<form
class="flex flex-row space-x-3"
hx-post="/todos"
hx-swap="beforebegin"
_="on submit target.reset()"
>
<select name="content" class="border border-black">
<option value="" disabled="true" selected="true">
Select a Todo
</option>
<option value="beth">Learn the BETH stack</option>
<option value="vim">Learn vim</option>
<option value="like">Like the video</option>
<option value="sub">Subscribe to Ethan</option>
</select>
<button type="submit">Add</button>
</form>
);
}

@ -9,6 +9,7 @@
"noEmit": true,
"composite": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react",