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
190 lines
6.5 KiB
Rust
190 lines
6.5 KiB
Rust
//! This crate contains all shared fullstack server functions.
|
|
use dioxus::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::{Mutex, LazyLock};
|
|
use chrono::{DateTime, Utc};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct Note {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub content: String,
|
|
pub tags: Vec<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
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();
|
|
Self {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
title,
|
|
content,
|
|
tags,
|
|
created_at: now,
|
|
updated_at: now,
|
|
is_pinned: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
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![
|
|
Note::new(
|
|
"Welcome to Braindump".to_string(),
|
|
"# Welcome\n\nThis is your braindump app. Quickly capture your thoughts, ideas, and notes here.\n\n## Features\n- Quick note capture\n- Markdown support\n- Tag and categorize\n- Search and filter\n- Pin important notes".to_string(),
|
|
vec!["welcome".to_string(), "guide".to_string()],
|
|
),
|
|
Note::new(
|
|
"Project Ideas".to_string(),
|
|
"- Build a personal dashboard\n- Create a habit tracker\n- Develop a recipe manager\n- Make a fitness app\n- Design a reading list organizer".to_string(),
|
|
vec!["ideas".to_string(), "projects".to_string()],
|
|
),
|
|
Note::new(
|
|
"Meeting Notes".to_string(),
|
|
"## Team Sync - Daily\n\nAttendees: Alice, Bob, Charlie\n\nTopics:\n1. Sprint progress review\n2. Blocker discussion\n3. Planning for next week\n\nAction items:\n- [ ] Review PR #123\n- [ ] Update documentation\n- [ ] Schedule follow-up meeting".to_string(),
|
|
vec!["work".to_string(), "meetings".to_string()],
|
|
),
|
|
])
|
|
});
|
|
|
|
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()))?;
|
|
let note = Note::new(title, content, tags);
|
|
notes.push(note.clone());
|
|
Ok(note.id)
|
|
}
|
|
|
|
#[get("/api/notes")]
|
|
pub async fn list_notes() -> Result<Vec<Note>, ServerFnError> {
|
|
let notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
Ok(notes.clone())
|
|
}
|
|
|
|
#[get("/api/notes/:id")]
|
|
pub async fn get_note(id: String) -> Result<Note, ServerFnError> {
|
|
let notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
notes
|
|
.iter()
|
|
.find(|n| n.id == id)
|
|
.cloned()
|
|
.ok_or_else(|| ServerFnError::new("Note not found".to_string()))
|
|
}
|
|
|
|
#[post("/api/notes/:id")]
|
|
pub async fn update_note(id: String, title: String, content: String, tags: Vec<String>, is_pinned: bool) -> Result<(), ServerFnError> {
|
|
let mut notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
if let Some(note) = notes.iter_mut().find(|n| n.id == id) {
|
|
note.title = title;
|
|
note.content = content;
|
|
note.tags = tags;
|
|
note.is_pinned = is_pinned;
|
|
note.updated_at = Utc::now();
|
|
Ok(())
|
|
} else {
|
|
Err(ServerFnError::new("Note not found".to_string()))
|
|
}
|
|
}
|
|
|
|
#[post("/api/notes/:id/delete")]
|
|
pub async fn delete_note(id: String) -> Result<(), ServerFnError> {
|
|
let mut notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
if notes.iter().any(|n| n.id == id) {
|
|
notes.retain(|n| n.id != id);
|
|
Ok(())
|
|
} else {
|
|
Err(ServerFnError::new("Note not found".to_string()))
|
|
}
|
|
}
|
|
|
|
#[post("/api/notes/:id/pin")]
|
|
pub async fn toggle_pin_note(id: String) -> Result<(), ServerFnError> {
|
|
let mut notes = NOTES.lock().map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
if let Some(note) = notes.iter_mut().find(|n| n.id == id) {
|
|
note.is_pinned = !note.is_pinned;
|
|
note.updated_at = Utc::now();
|
|
Ok(())
|
|
} else {
|
|
Err(ServerFnError::new("Note not found".to_string()))
|
|
}
|
|
}
|
|
|
|
#[post("/api/echo")]
|
|
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()))
|
|
}
|
|
}
|