From 6d92dff5e3e0d4e66470e7f0aa78bd66d4a73263 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 16 Mar 2026 16:24:10 -0400 Subject: [PATCH] feat(collections): add book picker modal and fix icon picker - Add book picker modal with Alpine.js state management for selecting books - Add toggleBookPickerBook, isBookPickerBookSelected, getBookPickerSelectedCount methods - Add clearBookPickerFilters function to reset filter form - Fix icon picker: add showAllIcons function to reset icon search - Fix setupHTMXModalInit to properly initialize Alpine tree after HTMX swap - Update collections template with book picker modal structure --- templates/collections.templ | 123 ++++++++++++++++++++++++++++++++- templates/collections_templ.go | 78 ++++++++++++++++----- web/src/collections.ts | 86 +++++++++++++++++++++-- 3 files changed, 260 insertions(+), 27 deletions(-) diff --git a/templates/collections.templ b/templates/collections.templ index bad3060..cf22697 100644 --- a/templates/collections.templ +++ b/templates/collections.templ @@ -1,6 +1,9 @@ package templates -import "bookhoard/internal/handlers" +import ( + "bookhoard/internal/handlers" + "fmt" +) templ Collection(user User, collections []CollectionData, errorMessage string) { @@ -12,7 +15,7 @@ templ Collection(user User, collections []CollectionData, errorMessage string) { - + @Header(user, "/collections") @@ -150,7 +153,10 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo > 🗑️ Remove Selected - @@ -255,6 +261,117 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo +
Collections - Bookhoard") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Collections - Bookhoard") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -57,7 +60,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 62, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 65, Col: 82} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -70,7 +73,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Color) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 66, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 69, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -83,7 +86,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 69, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 72, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -96,7 +99,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/collections/" + col.ID + "/edit-modal") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 72, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 75, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -109,7 +112,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/api/collections/" + col.ID + "") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 81, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 84, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -122,7 +125,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 91, Col: 92} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 94, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -135,7 +138,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 92, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 95, Col: 86} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -190,7 +193,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 109, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 112, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -211,7 +214,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 121, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 124, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -224,7 +227,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 123, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 126, Col: 90} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -237,13 +240,13 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 124, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 127, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Books in this Collection

0 selected
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

Books in this Collection

0 selected
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -261,7 +264,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 181, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 187, Col: 22} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -279,7 +282,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 188, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 194, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -302,7 +305,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(book.CoverImagePath) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 196, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 202, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -323,7 +326,46 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

Add Books to Collection

Search and select books to add to this collection.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

Add Books to Collection

Search and select books to add to this collection.

Add Books to Collection

books selected
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/collections.ts b/web/src/collections.ts index 48304ad..f68c96a 100644 --- a/web/src/collections.ts +++ b/web/src/collections.ts @@ -7,14 +7,17 @@ let collectionId: string | null = null; function initializeCollectionWebSocket(): void { const dataEl = document.getElementById("collection-data"); if (!dataEl) return; - + collectionId = dataEl.dataset.id || null; - + if (!collectionId) return; createWebSocket({ onMessage: (message) => { - if (message.type === "collection_updated" && message.data.collection_id === collectionId) { + if ( + message.type === "collection_updated" && + message.data.collection_id === collectionId + ) { const actionText = message.data.action === "books_added" ? `Added ${message.data.count || 0} book(s)` @@ -329,14 +332,37 @@ function initColorSelection(): void { // Initialize icon grid when modal is loaded via HTMX function setupHTMXModalInit(): void { - document.body.addEventListener("htmx:afterSwap", function (evt: Event) { - const target = (evt as any).detail.target; + document.body.addEventListener("htmx:afterSwap", function (evt: CustomEvent) { + const target = evt.detail.target; if (target && target.id === "modal-container") { - initIconSelection(); + populateIconGrid(); + Alpine.initTree(target); } }); } +function showAllIcons(): void { + // Populate grid if empty + populateIconGrid(); + + // Clear search filter + const searchInput = document.getElementById( + "icon-search", + ) as HTMLInputElement; + if (searchInput) { + searchInput.value = ""; + } + + // Show all icons + const iconGrid = document.getElementById("icon-grid"); + if (!iconGrid) return; + + const buttons = iconGrid.querySelectorAll(".icon-btn"); + buttons.forEach((btn) => { + (btn as HTMLElement).style.display = ""; + }); +} + // Icon data with keywords (single source of truth) const iconData: Record = { // Books & Reading @@ -463,10 +489,56 @@ export { selectColor, selectIcon, setupHTMXAuth, + setupHTMXModalInit, + showAllIcons, testRule, }; Alpine.data("collections", () => ({ + // Book Picker State + bookPickerOpen: false, + bookPickerSelected: [] as string[], + // Book Picker Methods + openBookPicker(): void { + this.bookPickerOpen = true; + this.bookPickerSelected = []; + }, + closeBookPicker(): void { + this.bookPickerOpen = false; + this.bookPickerSelected = []; + }, + toggleBookPickerBook(bookId: string): void { + const index = this.bookPickerSelected.indexOf(bookId); + if (index > -1) { + this.bookPickerSelected.splice(index, 1); + } else { + this.bookPickerSelected.push(bookId); + } + }, + isBookPickerBookSelected(bookId: string): boolean { + return this.bookPickerSelected.includes(bookId); + }, + getBookPickerSelectedCount(): number { + return this.bookPickerSelected.length; + }, + clearBookPickerFilters(): void { + const filterForm = document.getElementById( + "book-picker-filters", + ) as HTMLFormElement; + if (!filterForm) return; + + const inputs = filterForm.querySelectorAll("input:not([type='checkbox'])"); + inputs.forEach((input) => { + (input as HTMLInputElement).value = ""; + }); + + const grid = document.getElementById("book-picker-grid"); + if (grid) { + htmx.trigger(grid, "refresh"); + } + }, + + // Collection Methods closeCollectionModal, createRule, deleteRule, @@ -479,5 +551,7 @@ Alpine.data("collections", () => ({ populateIconGrid, selectColor, selectIcon, + setupHTMXModalInit, + showAllIcons, testRule, }));