use crate::queries::{ListTodos, Todo}; use livevue_rs::{RenderContext, global, mangle}; use maud::{html, Markup}; pub async fn todo_list(cx: &mut RenderContext) -> anyhow::Result { // Fetch all todos (registers CacheKey::Table { table: "todos" }) let todos = cx.run(ListTodos).await?; // Read the clock from KV (registers CacheKey::Channel { key: "clock" }) let clock = cx .kv_run("clock") .await .and_then(|v| v.as_str().map(|s| s.to_string())) .unwrap_or_else(|| "—".to_string()); let new_todo = cx.signal("newTodo", global()); let todo_count = todos.len(); let done_count = todos.iter().filter(|t| t.completed).count(); Ok(html! { div.todo-app { h1 { "LiveVue.rs — Todo List" } nav { span.nav-current { "Todos" } " | " button.nav-link data-on:click="$tree = {page: 'About'}" { "About" } } p.clock { "🕐 " (clock) } div.add-form { input type="text" placeholder="What needs to be done?" data-bind="newTodo" ; button data-on:click="@post('/action/add_todo')" data-attr=(format!("{{disabled: {}.trim() === ''}}", new_todo.val)) { "Add" } } p.stats { (format!("{} items, {} completed", todo_count, done_count)) } ul.todo-list { @for todo in &todos { (todo_item(cx, todo)) } } @if todos.is_empty() { p.empty { "No todos yet. Add one above!" } } } }) } fn todo_item(cx: &mut RenderContext, todo: &Todo) -> Markup { let _editing = cx.signal("editing", mangle(todo.id).client_only()); let title_class = if todo.completed { "todo-title completed" } else { "todo-title" }; html! { li.todo-item id=(format!("todo-{}", todo.id)) { input type="checkbox" checked[todo.completed] data-on:click=(format!("@post('/action/toggle_todo?id={}')", todo.id)) ; span class=(title_class) { (todo.title) } button.delete-btn data-on:click=(format!("@post('/action/delete_todo?id={}')", todo.id)) { "×" } } } }