From 4480fb781741a8e8da5f8e15cd119961019e9da4 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 9 Mar 2026 16:46:31 -0400 Subject: [PATCH] refactor: Consolidate header JavaScript files into main.js bundle - Update header.templ to load single main.js script instead of 4 separate files - Remove obsolete header.js and search.js as they are now bundled - Update Alpine.js event handlers to use standard double quotes - This completes the ESBuild migration by eliminating inline script loads The main.js bundle now contains all header, theme, and search functionality previously loaded separately, improving load performance and maintainability. --- templates/header.templ | 19 ++- web/static/header.js | 84 ------------- web/static/search.js | 267 ----------------------------------------- 3 files changed, 8 insertions(+), 362 deletions(-) delete mode 100644 web/static/header.js delete mode 100644 web/static/search.js diff --git a/templates/header.templ b/templates/header.templ index a90d485..a1b05c7 100644 --- a/templates/header.templ +++ b/templates/header.templ @@ -53,31 +53,31 @@ templ Header(user User, currentPath string) {

Select Theme

- - - - - - - @@ -193,8 +193,5 @@ templ Header(user User, currentPath string) {
- - - - + } diff --git a/web/static/header.js b/web/static/header.js deleted file mode 100644 index 6b68c01..0000000 --- a/web/static/header.js +++ /dev/null @@ -1,84 +0,0 @@ -// Header functionality -import { Alpine } from "./alpine"; -import { updateThemeIndicators } from "./themeDropdown"; -const toggleThemeDropdown = () => { - const dropdown = document.getElementById("theme-dropdown"); - if (dropdown) { - dropdown.classList.toggle("hidden"); - // Close user menu if open - const userMenu = document.getElementById("user-menu"); - if (userMenu && !dropdown.classList.contains("hidden")) { - userMenu.classList.add("hidden"); - } - } -}; -const toggleUserMenu = () => { - const menu = document.getElementById("user-menu"); - if (menu) { - menu.classList.toggle("hidden"); - // Close theme dropdown if open - const themeDropdown = document.getElementById("theme-dropdown"); - if (themeDropdown && !menu.classList.contains("hidden")) { - themeDropdown.classList.add("hidden"); - } - } -}; -const changeThemeTo = (theme) => { - // Apply the theme using the consolidated function from theme.ts - if (window.applyTheme) { - window.applyTheme(theme); - } - // Save to server if logged in - const token = localStorage.getItem("token"); - if (token) { - fetch("/api/auth/theme", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ theme }), - }).catch((err) => console.log("Theme save failed", err)); - } - // Close dropdown - const dropdown = document.getElementById("theme-dropdown"); - if (dropdown) { - dropdown.classList.add("hidden"); - } -}; -const logout = () => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - window.location.href = "/"; -}; -// Close dropdowns when clicking outside -document.addEventListener("click", (e) => { - const target = e.target; - const themeDropdown = document.getElementById("theme-dropdown"); - const userMenu = document.getElementById("user-menu"); - const themeButton = target?.closest('button[onclick="toggleThemeDropdown()"]'); - const userButton = target?.closest('button[onclick="toggleUserMenu()"]'); - if (!themeButton && - themeDropdown && - !themeDropdown.classList.contains("hidden")) { - if (!themeDropdown.contains(target)) { - themeDropdown.classList.add("hidden"); - } - } - if (!userButton && userMenu && !userMenu.classList.contains("hidden")) { - if (!userMenu.contains(target)) { - userMenu.classList.add("hidden"); - } - } -}); -export { toggleThemeDropdown, toggleUserMenu, changeThemeTo, logout }; -Alpine.global("header", { - logout, - toggleThemeDropdown, - toggleUserMenu, - changeThemeTo: (theme) => { - changeThemeTo(theme); - updateThemeIndicators(); // Call themeDropdown function - }, -}); -//# sourceMappingURL=header.js.map \ No newline at end of file diff --git a/web/static/search.js b/web/static/search.js deleted file mode 100644 index 1a83a44..0000000 --- a/web/static/search.js +++ /dev/null @@ -1,267 +0,0 @@ -let searchInputTimeout = null; -const SEARCH_DEBOUNCE_MS = 300; -const SEARCH_MIN_CHARS = 2; -function initializeSearch() { - const searchInput = document.getElementById("header-search"); - if (!searchInput) { - console.warn("Search input not found"); - return; - } - searchInput.addEventListener("input", handleSearchInput); - searchInput.addEventListener("keydown", handleSearchKeydown); - searchInput.addEventListener("focus", () => { - if (searchInput.value.length >= SEARCH_MIN_CHARS) { - performSearch(searchInput.value); - } - }); - document.addEventListener("click", (e) => { - const searchResults = document.getElementById("search-results"); - const searchInputEl = document.getElementById("header-search"); - if (searchResults && - !searchResults.contains(e.target) && - e.target !== searchInputEl) { - hideSearchResults(); - } - }); -} -function handleSearchInput(e) { - const target = e.target; - const query = target.value.trim(); - if (searchInputTimeout) { - clearTimeout(searchInputTimeout); - } - if (query.length < SEARCH_MIN_CHARS) { - hideSearchResults(); - return; - } - searchInputTimeout = setTimeout(() => { - performSearch(query); - }, SEARCH_DEBOUNCE_MS); -} -function handleSearchKeydown(e) { - const searchResults = document.getElementById("search-results"); - if (!searchResults || searchResults.classList.contains("hidden")) { - return; - } - const items = searchResults.querySelectorAll(".search-result-item"); - const currentIndex = parseInt(searchResults.dataset.selectedIndex || "-1"); - if (e.key === "ArrowDown") { - e.preventDefault(); - const nextIndex = Math.min(currentIndex + 1, items.length - 1); - selectSearchResult(items, nextIndex); - } - else if (e.key === "ArrowUp") { - e.preventDefault(); - const prevIndex = Math.max(currentIndex - 1, -1); - selectSearchResult(items, prevIndex); - } - else if (e.key === "Enter") { - e.preventDefault(); - if (currentIndex >= 0 && items[currentIndex]) { - const link = items[currentIndex].querySelector("a"); - if (link) - link.click(); - } - } - else if (e.key === "Escape") { - hideSearchResults(); - } -} -function selectSearchResult(items, index) { - items.forEach((item, i) => { - if (i === index) { - item.classList.add("bg-opacity-80"); - } - else { - item.classList.remove("bg-opacity-80"); - } - }); - const searchResults = document.getElementById("search-results"); - if (searchResults) { - searchResults.dataset.selectedIndex = index.toString(); - } -} -function performSearch(query) { - const token = localStorage.getItem("token"); - if (!token) { - console.warn("No authentication token found"); - return; - } - showSearchLoading(); - fetch(`/api/media-items/search?q=${encodeURIComponent(query)}`, { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }) - .then((response) => { - if (response.status === 404) { - return { error: "no results found", results: [] }; - } - return response.json(); - }) - .then((data) => { - hideSearchLoading(); - if (data && "error" in data && data.error === "no results found") { - showNoResults(query); - } - else if (Array.isArray(data) && data.length > 0) { - showSearchResults(data, query); - } - else if (Array.isArray(data)) { - showNoResults(query); - } - else { - showNoResults(query); - } - }) - .catch((error) => { - hideSearchLoading(); - console.error("Search error:", error); - showSearchError(); - }); -} -function showSearchLoading() { - createSearchResultsContainer(); - const searchResults = document.getElementById("search-results"); - if (!searchResults) - return; - searchResults.innerHTML = ` -
-
-

Searching...

-
- `; - searchResults.classList.remove("hidden"); -} -function hideSearchLoading() { } -function showSearchResults(results, query) { - createSearchResultsContainer(); - const searchResults = document.getElementById("search-results"); - if (!searchResults) - return; - searchResults.dataset.selectedIndex = "-1"; - const libraryIconMap = { - ebooks: "📚", - comics: "📖", - manga: "🗾", - }; - let html = ` -
-

- ${results.length} result${results.length !== 1 ? "s" : ""} for "${searchEscapeHtml(query)}" -

-
-
- `; - results.forEach((item, index) => { - const icon = libraryIconMap[item.library_type_name] || "📁"; - const titleHtml = highlightMatch(item.title, query); - const authorHtml = item.author ? highlightMatch(item.author, query) : ""; - html += ` - - `; - }); - html += ` -
-
-

- Press ↑↓ to navigate, - Enter to select -

-
- `; - searchResults.innerHTML = html; - searchResults.classList.remove("hidden"); -} -function showNoResults(query) { - createSearchResultsContainer(); - const searchResults = document.getElementById("search-results"); - if (!searchResults) - return; - searchResults.innerHTML = ` -
-
🔍
-

No results found for "${searchEscapeHtml(query)}"

-

Try different keywords

-
- `; - searchResults.classList.remove("hidden"); -} -function showSearchError() { - createSearchResultsContainer(); - const searchResults = document.getElementById("search-results"); - if (!searchResults) - return; - searchResults.innerHTML = ` -
-
⚠️
-

Search error

-

Please try again

-
- `; - searchResults.classList.remove("hidden"); -} -function hideSearchResults() { - const searchResults = document.getElementById("search-results"); - if (searchResults) { - searchResults.classList.add("hidden"); - } -} -function createSearchResultsContainer() { - let searchResults = document.getElementById("search-results"); - if (!searchResults) { - searchResults = document.createElement("div"); - searchResults.id = "search-results"; - searchResults.className = - "hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border"; - searchResults.style.cssText = - "background-color: var(--bg-secondary); border-color: var(--border)"; - const searchInput = document.getElementById("header-search"); - if (searchInput) { - const searchContainer = searchInput.closest(".relative"); - if (searchContainer) { - searchContainer.appendChild(searchResults); - } - } - } -} -function highlightMatch(text, query) { - if (!text) - return ""; - const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const regex = new RegExp(`(${escapedQuery})`, "gi"); - return searchEscapeHtml(text).replace(regex, '$1'); -} -function searchEscapeHtml(text) { - const div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; -} -function selectLibraryAndBook(libraryId, bookId) { - localStorage.setItem("selectedLibrary", libraryId); - localStorage.setItem("selectedBook", bookId); - hideSearchResults(); -} -document.addEventListener("DOMContentLoaded", initializeSearch); -window.selectLibraryAndBook = selectLibraryAndBook; -//# sourceMappingURL=search.js.map \ No newline at end of file