Template
1
0

feat: add sample todo with valkyr/db

This commit is contained in:
2025-08-16 17:12:55 +02:00
parent 0b0ecbcb79
commit 81108e0a60
14 changed files with 611 additions and 137 deletions

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>

View File

@@ -14,15 +14,17 @@
"@spec/schemas": "workspace:*",
"@tanstack/react-query": "5",
"@tanstack/react-router": "1",
"@valkyr/db": "1",
"@valkyr/db": "npm:@jsr/valkyr__db@2.0.0-beta.3",
"@valkyr/event-emitter": "npm:@jsr/valkyr__event-emitter@1",
"fast-equals": "5",
"react": "19",
"react-dom": "19",
"tailwindcss": "4",
"zod": "4"
},
"devDependencies": {
"@eslint/js": "9",
"@tailwindcss/vite": "4",
"@tanstack/react-router-devtools": "1",
"@types/react": "19",
"@types/react-dom": "19",

1
apps/react/src/index.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -1,3 +1,5 @@
import "./index.css";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

View File

@@ -2,6 +2,7 @@ import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { CreateAccountView } from "./views/account/create.view.tsx";
import { TodosView } from "./views/todo/todos.view.tsx";
const rootRoute = createRootRoute({
component: () => (
@@ -26,4 +27,10 @@ const createAccountRoute = createRoute({
component: CreateAccountView,
});
export const routeTree = rootRoute.addChildren([homeRoute, createAccountRoute]);
const todosRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/todos",
component: TodosView,
});
export const routeTree = rootRoute.addChildren([homeRoute, createAccountRoute, todosRoute]);

View File

@@ -0,0 +1,14 @@
import { IndexedDatabase } from "@valkyr/db";
import type { Todo } from "./todo.ts";
import type { TodoItem } from "./todo-item.ts";
import type { User } from "./user.ts";
export const db = new IndexedDatabase<{
todos: Todo;
todoItems: TodoItem;
users: User;
}>({
name: "app:valkyr",
registrars: [{ name: "todos", indexes: [["name", { unique: true }]] }, { name: "todoItems" }, { name: "users" }],
});

View File

@@ -0,0 +1,9 @@
import z from "zod";
export const TodoItemSchema = z.object({
id: z.string(),
task: z.string(),
completedAt: z.string(),
});
export type TodoItem = z.infer<typeof TodoItemSchema>;

View File

@@ -0,0 +1,11 @@
import z from "zod";
import { TodoItemSchema } from "./todo-item.ts";
export const TodoSchema = z.object({
id: z.string(),
name: z.string(),
items: z.array(TodoItemSchema).default([]),
});
export type Todo = z.infer<typeof TodoSchema>;

View File

@@ -0,0 +1,11 @@
import { ContactSchema } from "@spec/schemas/contact.ts";
import { NameSchema } from "@spec/schemas/name.ts";
import z from "zod";
export const UserSchema = z.object({
id: z.string(),
name: NameSchema,
contact: ContactSchema,
});
export type User = z.infer<typeof UserSchema>;

View File

@@ -0,0 +1,28 @@
import z from "zod";
import { Controller } from "../../libraries/controller.ts";
import { Form } from "../../libraries/form.ts";
import { db } from "../../stores/database.ts";
import type { Todo } from "../../stores/todo.ts";
const inputs = {
name: z.string(),
};
export class TodosController extends Controller<{
form: Form<typeof inputs>;
todos: Todo[];
}> {
override async onInit() {
return {
form: new Form(inputs).onSubmit(async ({ name }) => {
db.collection("todos").insertOne({ name, items: [] });
}),
todos: await this.query(db.collection("todos"), { limit: 10 }, "todos"),
};
}
remove(id: string) {
db.collection("todos").remove({ id });
}
}

View File

@@ -0,0 +1,72 @@
import { Link } from "@tanstack/react-router";
import { makeControllerView } from "../../libraries/view.ts";
import { TodosController } from "./todos.controller.ts";
export const TodosView = makeControllerView(
TodosController,
({ state: { form, todos }, actions: { remove, stress } }) => {
return (
<div className="min-h-screen bg-gray-100 flex flex-col items-center py-10 px-4 font-sans">
<div className="w-full max-w-2xl space-y-8">
{/* Heading */}
<header className="text-center">
<h1 className="text-3xl font-semibold text-gray-800">Todo Lists</h1>
<p className="text-gray-500 mt-2">Create and manage your collections of tasks</p>
</header>
{/* Create form */}
<form onSubmit={form.submit} className="flex gap-2 w-full">
<input
type="text"
placeholder="Enter todo list name..."
{...form.register("name")}
className="flex-1 px-3 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-800 bg-gray-50"
/>
<button
type="submit"
className="px-5 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white font-medium transition"
>
Create
</button>
</form>
{/* Todo list output */}
<section>
<h2 className="text-lg font-medium text-gray-700 mb-4">Your Lists</h2>
{todos?.length > 0 ? (
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white">
{todos.map((todo) => (
<li key={todo.id} className="flex items-center justify-between px-4 py-3">
{/* List name */}
<span className="text-gray-800 font-medium">{todo.name}</span>
{/* Actions */}
<div className="flex gap-2">
<Link
to="/todos/$id"
params={{ id: todo.id }}
className="px-3 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium transition"
>
Open
</Link>
<button
type="button"
onClick={() => remove(todo.id)}
className="px-3 py-1.5 rounded-lg bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition"
>
Delete
</button>
</div>
</li>
))}
</ul>
) : (
<p className="text-gray-500 text-sm">No todo lists yet. Create one above!</p>
)}
</section>
</div>
</div>
);
},
);

View File

@@ -0,0 +1,9 @@
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
}
}
}
}

View File

@@ -1,8 +1,9 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react()],
plugins: [react(), tailwindcss()],
server: {
proxy: {
"/api/v1": {