diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-13 14:20:41 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-13 14:20:41 -1000 |
| commit | a7a9aa3dcfe4b90d9b32791c8313a0019ad11289 (patch) | |
| tree | e04c67d6896275a773ad759d27820a1d445695a0 | |
| parent | e107192be5efb65807c7da3b6aa99ce3555944d0 (diff) | |
Implement Todoist write operations - Handlers & UI (Part 2)
Complete Todoist task creation and completion functionality:
Handlers:
- Update aggregateData to fetch and populate Projects
- Add HandleCreateTask: creates task, refreshes list, re-renders
- Add HandleCompleteTask: marks task complete, returns empty
- Both handlers pass Projects to template for dropdown
Routes:
- Register POST /tasks for task creation
- Register POST /tasks/complete for task completion
UI (todoist-tasks.html):
- Add Quick Add form with collapsible details element
- Project selector dropdown (iterates over .Projects)
- Content input field with validation
- HTMX integration: hx-post, hx-target, hx-swap
- Functional completion checkboxes on each task
- Remove disabled attribute from checkboxes
- Add todoist-task-item wrapper class for HTMX targeting
- Glassmorphism styling for form
Features:
- Create Todoist tasks with optional project assignment
- Mark tasks complete with single click (disappears)
- Real-time task list updates without page reload
- Seamless HTMX partial updates
All tests pass. Full Todoist write operations now live in UI!
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | cmd/dashboard/main.go | 4 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 101 | ||||
| -rw-r--r-- | web/templates/partials/todoist-tasks.html | 49 |
3 files changed, 150 insertions, 4 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index a307484..4a1fb32 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -81,6 +81,10 @@ func main() { r.Post("/cards", h.HandleCreateCard) r.Post("/cards/complete", h.HandleCompleteCard) + // Todoist task operations + r.Post("/tasks", h.HandleCreateTask) + r.Post("/tasks/complete", h.HandleCompleteTask) + // Serve static files fileServer := http.FileServer(http.Dir("web/static")) r.Handle("/static/*", http.StripPrefix("/static/", fileServer)) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 8762035..c3e49ed 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -254,6 +254,20 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models } }() + // Fetch Todoist projects + wg.Add(1) + go func() { + defer wg.Done() + projects, err := h.todoistClient.GetProjects(ctx) + mu.Lock() + defer mu.Unlock() + if err != nil { + log.Printf("Failed to fetch projects: %v", err) + } else { + data.Projects = projects + } + }() + // Fetch Obsidian notes (if configured) if h.obsidianClient != nil { wg.Add(1) @@ -528,3 +542,90 @@ func (h *Handler) HandleCompleteCard(w http.ResponseWriter, r *http.Request) { // Return empty response (card will be removed from DOM) w.WriteHeader(http.StatusOK) } + +// HandleCreateTask creates a new Todoist task +func (h *Handler) HandleCreateTask(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse form data + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + log.Printf("Error parsing form: %v", err) + return + } + + content := r.FormValue("content") + projectID := r.FormValue("project_id") + + if content == "" { + http.Error(w, "Missing content", http.StatusBadRequest) + return + } + + // Create the task + _, err := h.todoistClient.CreateTask(ctx, content, projectID, nil, 0) + if err != nil { + http.Error(w, "Failed to create task", http.StatusInternalServerError) + log.Printf("Error creating task: %v", err) + return + } + + // Force refresh to get updated tasks + tasks, err := h.fetchTasks(ctx, true) + if err != nil { + http.Error(w, "Failed to refresh tasks", http.StatusInternalServerError) + log.Printf("Error refreshing tasks: %v", err) + return + } + + // Fetch projects for the dropdown + projects, err := h.todoistClient.GetProjects(ctx) + if err != nil { + log.Printf("Failed to fetch projects: %v", err) + projects = []models.Project{} + } + + // Prepare data for template rendering + data := struct { + Tasks []models.Task + Projects []models.Project + }{ + Tasks: tasks, + Projects: projects, + } + + // Render the updated task list + if err := h.templates.ExecuteTemplate(w, "todoist-tasks", data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Error rendering todoist tasks template: %v", err) + } +} + +// HandleCompleteTask marks a Todoist task as complete +func (h *Handler) HandleCompleteTask(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse form data + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + log.Printf("Error parsing form: %v", err) + return + } + + taskID := r.FormValue("task_id") + + if taskID == "" { + http.Error(w, "Missing task_id", http.StatusBadRequest) + return + } + + // Mark task as complete + if err := h.todoistClient.CompleteTask(ctx, taskID); err != nil { + http.Error(w, "Failed to complete task", http.StatusInternalServerError) + log.Printf("Error completing task: %v", err) + return + } + + // Return empty response (task will be removed from DOM) + w.WriteHeader(http.StatusOK) +} diff --git a/web/templates/partials/todoist-tasks.html b/web/templates/partials/todoist-tasks.html index 7595ac7..25faf47 100644 --- a/web/templates/partials/todoist-tasks.html +++ b/web/templates/partials/todoist-tasks.html @@ -1,17 +1,58 @@ {{define "todoist-tasks"}} -<section class="card"> +<section class="card" id="todoist-list"> <!-- Section Header with Brand Color --> <div class="flex items-center gap-3 mb-6"> <div class="w-1 h-8 bg-todoist rounded"></div> <h2 class="text-2xl font-bold text-gray-900">Todoist Tasks</h2> </div> + <!-- Quick Add Form --> + {{if .Projects}} + <details class="mb-6" open> + <summary class="cursor-pointer text-sm text-indigo-600 hover:text-indigo-800 font-medium transition-colors mb-2"> + + Quick Add Task + </summary> + <form hx-post="/tasks" + hx-target="#todoist-list" + hx-swap="outerHTML" + class="mt-3 space-y-3 bg-white/40 p-4 rounded-lg"> + + <input type="text" + name="content" + placeholder="Task content" + required + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"> + + <select name="project_id" + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"> + <option value="">Select project (optional)...</option> + {{range .Projects}} + <option value="{{.ID}}">{{.Name}}</option> + {{end}} + </select> + + <button type="submit" + class="w-full bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"> + Add Task + </button> + </form> + </details> + {{end}} + + <!-- Tasks List --> {{if .Tasks}} <div class="space-y-3"> {{range .Tasks}} - <div class="task-item"> - <input type="checkbox" {{if .Completed}}checked{{end}} - class="mt-1 h-5 w-5 text-todoist rounded border-gray-300" disabled> + <div class="todoist-task-item task-item"> + <!-- Functional Checkbox --> + <input type="checkbox" + {{if .Completed}}checked{{end}} + hx-post="/tasks/complete" + hx-vals='{"task_id": "{{.ID}}"}' + hx-target="closest .todoist-task-item" + hx-swap="outerHTML" + class="mt-1 h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"> + <div class="flex-1"> <p class="font-medium text-gray-900 {{if .Completed}}line-through text-gray-500{{end}}"> {{.Content}} |
