feat: add sample todo with valkyr/db
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
1
apps/react/src/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./index.css";
|
||||
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
@@ -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]);
|
||||
|
||||
14
apps/react/src/stores/database.ts
Normal file
14
apps/react/src/stores/database.ts
Normal 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" }],
|
||||
});
|
||||
9
apps/react/src/stores/todo-item.ts
Normal file
9
apps/react/src/stores/todo-item.ts
Normal 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>;
|
||||
11
apps/react/src/stores/todo.ts
Normal file
11
apps/react/src/stores/todo.ts
Normal 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>;
|
||||
11
apps/react/src/stores/user.ts
Normal file
11
apps/react/src/stores/user.ts
Normal 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>;
|
||||
28
apps/react/src/views/todo/todos.controller.ts
Normal file
28
apps/react/src/views/todo/todos.controller.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
72
apps/react/src/views/todo/todos.view.tsx
Normal file
72
apps/react/src/views/todo/todos.view.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
9
apps/react/tailwind.config.js
Normal file
9
apps/react/tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user