diff --git a/web/src/collections.ts b/web/src/collections.ts index de213c2..f728938 100644 --- a/web/src/collections.ts +++ b/web/src/collections.ts @@ -206,3 +206,271 @@ function renderTestResults(results: unknown[]): void { (window as any).createRule = createRule; (window as any).deleteRule = deleteRule; (window as any).testRule = testRule; + +// Add authorization header to all HTMX requests +function setupHTMXAuth(): void { + document.body.addEventListener("htmx:configRequest", function (evt: Event) { + const token = localStorage.getItem("token"); + (evt as any).detail.headers["Authorization"] = `Bearer ${token}`; + }); +} +// Auto-initialize HTMX auth when DOM is ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupHTMXAuth); +} else { + setupHTMXAuth(); +} + +function navigateToCollection(element: HTMLElement): void { + // Check if the click target is a button + const event = window.event as Event; + if (event && event.target instanceof HTMLElement) { + const target = event.target as HTMLElement; + + // If user clicked a button or button icon, don't navigate + if (target.tagName === "BUTTON" || target.closest("button") !== null) { + return; // Let HTMX handle the button action + } + } + + // Only navigate if clicking the card body + const href = element.getAttribute("data-href"); + if (href) { + window.location.href = href; + } +} + +(window as any).navigateToCollection = navigateToCollection; + +// ============================================================================ +// Collection Modal UI Helpers +// ============================================================================ +const borderClasses: Record = { + blue: "border-blue-500", + red: "border-red-500", + yellow: "border-yellow-500", + green: "border-green-500", + purple: "border-purple-500", +}; + +// Color selection for create/edit modal +function selectColor(color: string): void { + const colorInput = document.getElementById( + "collection-color", + ) as HTMLInputElement; + if (colorInput) { + colorInput.value = color; + } + // Update visual selection + document.querySelectorAll(".color-option").forEach((btn) => { + (btn as HTMLElement).style.outline = "none"; + (btn as HTMLElement).style.outlineOffset = "0"; + }); + const selectedBtn = document.querySelector( + `.color-option[onclick="selectColor('${color}')"]`, + ) as HTMLElement; + if (selectedBtn) { + selectedBtn.style.outline = "3px solid var(--text-primary)"; + selectedBtn.style.outlineOffset = "3px"; + } +} +// Close modal (removes from DOM) +function closeCollectionModal(): void { + const modal = document.querySelector(".fixed.inset-0"); + if (modal && modal.parentElement) { + modal.parentElement.remove(); + } +} + +// Initialize color selection on page load +function initColorSelection(): void { + const colorInput = document.getElementById( + "collection-color", + ) as HTMLInputElement; + if (colorInput && colorInput.value) { + selectColor(colorInput.value); + } + + // Apply border color classes to collection cards + document.querySelectorAll("[data-color]").forEach((card) => { + const color = (card as HTMLElement).getAttribute("data-color"); + if (color && borderClasses[color]) { + // Remove old border color classes + Object.values(borderClasses).forEach((cls) => { + (card as HTMLElement).classList.remove(cls); + }); + // Add new border color class + (card as HTMLElement).classList.add(borderClasses[color]); + } + }); +} + +// Auto-initialize when DOM is ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initColorSelection); +} else { + initColorSelection(); +} +// Export functions for global access +(window as any).selectColor = selectColor; +(window as any).closeCollectionModal = closeCollectionModal; +(window as any).initColorSelection = initColorSelection; + +// 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; + if (target && target.id === "modal-container") { + initIconSelection(); + } + }); +} +// Auto-initialize when DOM is ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupHTMXModalInit); +} else { + setupHTMXModalInit(); +} + +// Icon data with keywords (single source of truth) +const iconData: Record = { + // Books & Reading + "📚": ["book", "books", "library", "read", "reading"], + "📖": ["book", "open", "read", "reading"], + "📝": ["memo", "note", "write", "writing", "edit"], + "📰": ["news", "newspaper", "press", "article"], + "🗂️": ["card", "index", "organize", "file"], + "📁": ["file", "folder", "directory"], + "📂": ["folder", "open", "directory"], + "📓": ["notebook", "book", "write"], + "📔": ["notebook", "book"], + "📕": ["book", "read", "red"], + "📗": ["book", "read", "green"], + "📘": ["book", "read", "blue"], + "📙": ["book", "read", "orange"], + + // Favorites & Activities + "⭐": ["star", "favorite", "like", "rating"], + "❤️": ["heart", "love", "favorite", "like"], + "🔥": ["fire", "hot", "popular", "trending", "flame"], + "💡": ["idea", "bulb", "light", "think", "smart"], + "🎯": ["target", "goal", "aim", "focus"], + "🏆": ["trophy", "winner", "award", "achievement"], + "🎨": ["art", "paint", "color", "creative"], + "✅": ["check", "done", "complete", "success"], + "❌": ["cross", "x", "wrong", "error", "fail"], + "⚡️": ["bolt", "fast", "quick", "energy"], + "🚀": ["rocket", "fast", "launch", "space"], + + // Places & Objects + "💎": ["gem", "diamond", "stone", "rich"], + "👍": ["thumb", "up", "good", "yes", "like"], + "👎": ["thumb", "down", "bad", "no", "dislike"], + "🏠": ["house", "home", "building"], + "🏢": ["building", "office", "work", "company"], + "🚗": ["car", "auto", "vehicle", "drive"], + "✈️": ["plane", "airplane", "fly", "travel"], + "🎮": ["game", "play", "video", "gaming"], +}; +// Helper: Get just the emoji list +const allIcons = Object.keys(iconData); + +function populateIconGrid(): void { + const iconGrid = document.getElementById("icon-grid"); + if (!iconGrid) return; + // Clear any existing content + iconGrid.innerHTML = ""; + // Generate buttons from iconData + Object.entries(iconData).forEach(([emoji, keywords]) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "icon-btn text-2xl p-1 hover:bg-opacity-80 rounded"; + button.textContent = emoji; + button.setAttribute("onclick", `selectIcon('${emoji}')`); + button.setAttribute("data-keywords", keywords.join(",")); + iconGrid.appendChild(button); + }); +} + +function selectIcon(icon: string): void { + const iconInput = document.getElementById( + "collection-icon", + ) as HTMLInputElement; + const searchInput = document.getElementById( + "icon-search", + ) as HTMLInputElement; + + if (iconInput) iconInput.value = icon; + if (searchInput) searchInput.value = icon; + + // Visual feedback + document.querySelectorAll(".icon-btn").forEach((btn) => { + (btn as HTMLElement).style.outline = "none"; + (btn as HTMLElement).style.backgroundColor = ""; + }); + + const selectedBtn = document.querySelector( + `.icon-btn[onclick="selectIcon('${icon}')"]`, + ) as HTMLElement; + if (selectedBtn) { + selectedBtn.style.outline = "2px solid var(--text-primary)"; + selectedBtn.style.backgroundColor = "var(--bg-secondary)"; + } +} +function filterIcons(searchTerm: string): void { + const iconGrid = document.getElementById("icon-grid"); + const iconInput = document.getElementById( + "collection-icon", + ) as HTMLInputElement; + // Update hidden input with typed value + if (iconInput) iconInput.value = searchTerm; + if (!iconGrid) return; + const buttons = iconGrid.querySelectorAll(".icon-btn"); + const lowerSearchTerm = searchTerm.toLowerCase(); + + buttons.forEach((btn) => { + const emoji = (btn as HTMLElement).textContent || ""; + const keywords = iconData[emoji] || []; + + // Search in keywords OR emoji itself + const matches = + searchTerm === "" || + emoji.includes(searchTerm) || + keywords.some((keyword) => + keyword.toLowerCase().includes(lowerSearchTerm), + ); + + (btn as HTMLElement).style.display = matches ? "" : "none"; + }); +} +function showAllIcons(): void { + const iconGrid = document.getElementById("icon-grid"); + if (!iconGrid) return; + + const buttons = iconGrid.querySelectorAll(".icon-btn"); + buttons.forEach((btn) => { + (btn as HTMLElement).style.display = ""; + }); +} +function initIconSelection(): void { + // First, populate the grid with all icons + populateIconGrid(); + // Then, set the current selection + const iconInput = document.getElementById( + "collection-icon", + ) as HTMLInputElement; + const searchInput = document.getElementById( + "icon-search", + ) as HTMLInputElement; + + if (iconInput && iconInput.value && searchInput) { + searchInput.value = iconInput.value; + selectIcon(iconInput.value); + } +} +// Export for global access +(window as any).selectIcon = selectIcon; +(window as any).filterIcons = filterIcons; +(window as any).showAllIcons = showAllIcons; +(window as any).populateIconGrid = populateIconGrid; +(window as any).initIconSelection = initIconSelection;