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.
This commit is contained in:
+8
-11
@@ -53,31 +53,31 @@ templ Header(user User, currentPath string) {
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">Select Theme</h3>
|
||||
<div class="space-y-2">
|
||||
<button @click=\"changeThemeTo('tokyo-night')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('tokyo-night')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #7aa2f7;"></span>
|
||||
Tokyo Night
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('dracula')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('dracula')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #bd93f9;"></span>
|
||||
Dracula
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('nord')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('nord')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #88c0d0;"></span>
|
||||
Nord
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('solarized-dark')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('solarized-dark')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #2aa198;"></span>
|
||||
Solarized Dark
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('monokai')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('monokai')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #a6e22e;"></span>
|
||||
Monokai
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('one-dark-pro')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('one-dark-pro')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #61dafb;"></span>
|
||||
One Dark Pro
|
||||
</button>
|
||||
<button @click=\"changeThemeTo('material-dark')\" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<button @click="changeThemeTo('material-dark')" class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
||||
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #80cbc4;"></span>
|
||||
Material Dark
|
||||
</button>
|
||||
@@ -193,8 +193,5 @@ templ Header(user User, currentPath string) {
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script src="/static/theme.js"></script>
|
||||
<script src="/static/woodPaneling.js"></script>
|
||||
<script src="/static/themeDropdown.js"></script>
|
||||
<script src="/static/header.js"></script>
|
||||
<script src="/static/main.js"></script>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 = `
|
||||
<div class="p-4 text-center" style="color: var(--text-secondary)">
|
||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2" style="border-color: var(--accent)"></div>
|
||||
<p class="mt-2 text-sm">Searching...</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="p-3 border-b" style="border-color: var(--border)">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">
|
||||
${results.length} result${results.length !== 1 ? "s" : ""} for "${searchEscapeHtml(query)}"
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
`;
|
||||
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 += `
|
||||
<div class="search-result-item p-3 border-b hover:bg-opacity-50 transition-colors cursor-pointer"
|
||||
style="border-color: var(--border); background-color: var(--bg-secondary)"
|
||||
data-index="${index}">
|
||||
<a href="/bookshelf"
|
||||
class="block"
|
||||
onclick="window.selectLibraryAndBook('${item.library_id}', '${item.id}')">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="text-2xl">${icon}</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium truncate" style="color: var(--text-primary)">
|
||||
${titleHtml}
|
||||
</h4>
|
||||
${authorHtml ? `<p class="text-xs truncate" style="color: var(--text-secondary)">${authorHtml}</p>` : ""}
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">
|
||||
${searchEscapeHtml(item.library_name)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += `
|
||||
</div>
|
||||
<div class="p-2 border-t text-center" style="border-color: var(--border)">
|
||||
<p class="text-xs" style="color: var(--text-secondary)">
|
||||
Press <kbd class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary)">↑↓</kbd> to navigate,
|
||||
<kbd class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary)">Enter</kbd> to select
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
searchResults.innerHTML = html;
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function showNoResults(query) {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.innerHTML = `
|
||||
<div class="p-4 text-center">
|
||||
<div class="text-4xl mb-2">🔍</div>
|
||||
<p class="text-sm" style="color: var(--text-primary)">No results found for "${searchEscapeHtml(query)}"</p>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Try different keywords</p>
|
||||
</div>
|
||||
`;
|
||||
searchResults.classList.remove("hidden");
|
||||
}
|
||||
function showSearchError() {
|
||||
createSearchResultsContainer();
|
||||
const searchResults = document.getElementById("search-results");
|
||||
if (!searchResults)
|
||||
return;
|
||||
searchResults.innerHTML = `
|
||||
<div class="p-4 text-center">
|
||||
<div class="text-4xl mb-2">⚠️</div>
|
||||
<p class="text-sm" style="color: var(--text-primary)">Search error</p>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Please try again</p>
|
||||
</div>
|
||||
`;
|
||||
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, '<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>');
|
||||
}
|
||||
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
|
||||
Reference in New Issue
Block a user