Unify history note into a single Alpine component
Two conditional divs and a separate adding scope replaced by
one historyNote component with reactive hasNote/editing state.

Assisted-by: Claude Opus 4.6 via Claude Code
change soypxnrnqmxrzurvvuxmyuxtkoykrutp
commit c584ddce75db1265f3c355e0e1530f3f1022fecd
author Alpha Chen <alpha@kejadlen.dev>
date
parent kqwwnlzp
diff --git a/lib/ketchup/views/dashboard.rb b/lib/ketchup/views/dashboard.rb
index fe79274..0c22622 100644
--- a/lib/ketchup/views/dashboard.rb
+++ b/lib/ketchup/views/dashboard.rb
@@ -145,35 +145,25 @@ module Views
               h3 { "History" }
               ul do
                 @series.completed_tasks.each do |ct|
-                  li(class: "task-history-item") do
+                  li(
+                    class: "task-history-item",
+                    "x-data": "historyNote(#{ct[:id]}, #{ct[:note] ? "true" : "false"})"
+                  ) do
                     div(class: "task-history-row") do
                       span(class: "task-history-check") { "✓" }
                       span(class: "task-history-date") { ct[:completed_at].strftime("%Y-%m-%d") }
-                      if ct[:note].nil?
-                        span(
-                          class: "task-history-add-note",
-                          "x-data": "{ adding: false }",
-                          "x-show": "!adding",
-                          "x-on:click": "adding = true; $dispatch('add-note-#{ct[:id]}')",
-                          "x-on:reset-note-#{ct[:id]}.window": "adding = false"
-                        ) { "add a note..." }
-                      end
+                      span(
+                        class: "task-history-add-note",
+                        "x-show": "!hasNote && !editing",
+                        "x-on:click": "edit()"
+                      ) { "add a note..." }
                     end
                     div(
                       class: "task-history-note-editor",
-                      "data-task-id": ct[:id].to_s,
                       "data-value": ct[:note] || "",
-                      "x-data": "historyNoteEditor",
-                      "x-init": "activate()"
-                    ) if ct[:note]
-                    div(
-                      class: "task-history-note-editor",
-                      "data-task-id": ct[:id].to_s,
-                      "data-value": "",
-                      "x-data": "historyNoteEditor",
-                      style: "display: none",
-                      "x-on:add-note-#{ct[:id]}.window": "show($el); activate()"
-                    ) if ct[:note].nil?
+                      "x-show": "hasNote || editing",
+                      "x-ref": "editor"
+                    )
                   end
                 end
               end
diff --git a/public/js/app.js b/public/js/app.js
index eb660f8..bc68567 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -105,20 +105,25 @@ document.addEventListener("alpine:init", () => {
     },
   }))
 
-  Alpine.data("historyNoteEditor", () => ({
+  Alpine.data("historyNote", (taskId, hasNote) => ({
+    hasNote,
+    editing: false,
     _editor: null,
 
-    show(el) {
-      el.style.display = ""
+    init() {
+      if (this.hasNote) this._initEditor()
     },
 
-    activate() {
-      const el = this.$el
-      const taskId = el.dataset.taskId
-      const initialNote = el.dataset.value || ""
+    edit() {
+      this.editing = true
+      this.$nextTick(() => this._initEditor())
+    },
 
-      if (this._editor) return
+    _initEditor() {
+      const el = this.$refs.editor
+      if (!el || this._editor) return
 
+      const initialNote = el.dataset.value || ""
       const [editor] = new OverType(el, {
         value: initialNote,
         placeholder: "Add a note...",
@@ -136,28 +141,19 @@ document.addEventListener("alpine:init", () => {
         if (!initialNote) textarea.focus()
         textarea.addEventListener("blur", () => {
           const note = editor.getValue().trim()
-
-          if (!note) {
-            if (initialNote) {
-              fetch(`/tasks/${taskId}/note`, {
-                method: "PATCH",
-                headers: { "Content-Type": "application/x-www-form-urlencoded" },
-                body: `note=`,
-              })
-            }
-            el.style.display = "none"
-            window.dispatchEvent(new Event(`reset-note-${taskId}`))
+          if (note === initialNote.trim()) {
+            if (!note) this.editing = false
             return
           }
 
-          if (note === (initialNote || "").trim()) return
-
-          el.dataset.value = note
           fetch(`/tasks/${taskId}/note`, {
             method: "PATCH",
             headers: { "Content-Type": "application/x-www-form-urlencoded" },
             body: `note=${encodeURIComponent(note)}`,
           })
+          el.dataset.value = note
+          this.hasNote = !!note
+          this.editing = false
         })
       }
     },