feat: add auto-save debounce, note refetch on switch, todos feature with markdown

Features implemented:
- Auto-save with counter-based debounce in note editor
- Note refetches when switching between notes (note_id prop changes)
- Todos feature with full CRUD operations
- Markdown rendering in todo titles
- API endpoints for todos (create, list, toggle, delete)
- Todo panel in sidebar with sorting (incomplete first, newest first)
- Todo items use TodoItem component for clean separation

Technical changes:
- Added Todo struct and API endpoints in api package
- Added markdown dependency to web package
- Implemented TodoPanel component with TodoItem sub-component
- Added mut keywords to signal bindings for Dioxus 0.7
- Fixed closure capture issues with cloning todo objects
This commit is contained in:
2026-02-04 13:29:33 +01:00
parent 25e498aff0
commit 3774c3347c
5 changed files with 454 additions and 7 deletions

View File

@@ -15,6 +15,15 @@ pub struct Note {
pub is_pinned: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Todo {
pub id: String,
pub title: String,
pub completed: bool,
pub created_at: DateTime<Utc>,
pub due_date: Option<DateTime<Utc>>,
}
impl Note {
pub fn new(title: String, content: String, tags: Vec<String>) -> Self {
let now = Utc::now();
@@ -30,7 +39,21 @@ impl Note {
}
}
impl Todo {
pub fn new(title: String, due_date: Option<DateTime<Utc>>) -> Self {
let now = Utc::now();
Self {
id: uuid::Uuid::new_v4().to_string(),
title,
completed: false,
created_at: now,
due_date,
}
}
}
type NotesStore = Mutex<Vec<Note>>;
type TodosStore = Mutex<Vec<Todo>>;
static NOTES: LazyLock<NotesStore> = LazyLock::new(|| {
Mutex::new(vec![
@@ -52,6 +75,14 @@ static NOTES: LazyLock<NotesStore> = LazyLock::new(|| {
])
});
static TODOS: LazyLock<TodosStore> = LazyLock::new(|| {
Mutex::new(vec![
Todo::new("Review project proposal".to_string(), Some(Utc::now() + chrono::Duration::days(2))),
Todo::new("Complete documentation".to_string(), Some(Utc::now() + chrono::Duration::days(5))),
Todo::new("Team meeting preparation".to_string(), Some(Utc::now() + chrono::Duration::days(1))),
])
});
#[post("/api/notes")]
pub async fn create_note(title: String, content: String, tags: Vec<String>) -> Result<String, ServerFnError> {
let mut notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
@@ -118,3 +149,41 @@ pub async fn toggle_pin_note(id: String) -> Result<(), ServerFnError> {
pub async fn echo(message: String) -> Result<String, ServerFnError> {
Ok(format!("Echo: {}", message))
}
// Todo endpoints
#[post("/api/todos")]
pub async fn create_todo(title: String, due_date: Option<String>) -> Result<String, ServerFnError> {
let mut todos = TODOS.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
let due_date_parsed = due_date.and_then(|d| chrono::DateTime::parse_from_rfc3339(&d).ok()).map(|d| d.with_timezone(&Utc));
let todo = Todo::new(title, due_date_parsed);
todos.push(todo.clone());
Ok(todo.id)
}
#[get("/api/todos")]
pub async fn list_todos() -> Result<Vec<Todo>, ServerFnError> {
let todos = TODOS.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
Ok(todos.clone())
}
#[post("/api/todos/:id/toggle")]
pub async fn toggle_todo(id: String) -> Result<(), ServerFnError> {
let mut todos = TODOS.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
if let Some(todo) = todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
Ok(())
} else {
Err(ServerFnError::new("Todo not found".to_string()))
}
}
#[post("/api/todos/:id/delete")]
pub async fn delete_todo(id: String) -> Result<(), ServerFnError> {
let mut todos = TODOS.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
if todos.iter().any(|t| t.id == id) {
todos.retain(|t| t.id != id);
Ok(())
} else {
Err(ServerFnError::new("Todo not found".to_string()))
}
}