summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/dashboard/main.go4
-rw-r--r--internal/handlers/handlers.go101
-rw-r--r--web/templates/partials/todoist-tasks.html49
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}}