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
This commit is contained in:
2026-03-16 16:24:10 -04:00
parent 5782a4e314
commit 6d92dff5e3
3 changed files with 260 additions and 27 deletions
+80 -6
View File
@@ -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<string, string[]> = {
// 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,
}));