build: Add HTMX copy step to build:ts script
- Modified package.json build:ts to copy htmx.min.js from node_modules to web/static/ - This fixes the 404 error for /static/htmx.min.js that occurred after ESBuild migration - Added htmx.min.js to static files See ESBUILD_MIGRATION_PLAN.md for migration context.
This commit is contained in:
+8
-7
@@ -5,24 +5,25 @@
|
||||
"scripts": {
|
||||
"build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch",
|
||||
"build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify",
|
||||
"build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
|
||||
"build:ts:dev": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
|
||||
"build:ts:watch": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --watch",
|
||||
"build:ts": "cp node_modules/htmx.org/dist/htmx.min.js web/static/htmx.min.js && esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
|
||||
"build:ts:dev": "cp node_modules/htmx.org/dist/htmx.min.js web/static/htmx.min.js && esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
|
||||
"build:ts:watch": "cp node_modules/htmx.org/dist/htmx.min.js web/static/htmx.min.js && esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --watch",
|
||||
"build": "npm run build:ts && npm run build:css:prod",
|
||||
"dev": "npm run build:ts:dev && npm run build:css"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.15.8",
|
||||
"lunr": "^2.3.9",
|
||||
"chart.js": "^4.5.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"htmx.org": "^2.0.8",
|
||||
"highlight.js": "^11.11.1"
|
||||
"lunr": "^2.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"esbuild": "^0.27.3",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"esbuild": "^0.27.3"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
+83
-1
@@ -1,2 +1,84 @@
|
||||
(()=>{var i=()=>{let t=document.getElementById("theme-dropdown");if(t){t.classList.toggle("hidden");let e=document.getElementById("user-menu");e&&!t.classList.contains("hidden")&&e.classList.add("hidden")}},a=()=>{let t=document.getElementById("user-menu");if(t){t.classList.toggle("hidden");let e=document.getElementById("theme-dropdown");e&&!t.classList.contains("hidden")&&e.classList.add("hidden")}},c=t=>{window.applyTheme&&window.applyTheme(t);let e=localStorage.getItem("token");e&&fetch("/api/auth/theme",{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({theme:t})}).catch(o=>console.log("Theme save failed",o));let n=document.getElementById("theme-dropdown");n&&n.classList.add("hidden")},l=()=>{localStorage.removeItem("token"),localStorage.removeItem("user"),window.location.href="/"};document.addEventListener("click",t=>{let e=t.target,n=document.getElementById("theme-dropdown"),o=document.getElementById("user-menu"),s=e?.closest('button[onclick="toggleThemeDropdown()"]'),d=e?.closest('button[onclick="toggleUserMenu()"]');!s&&n&&!n.classList.contains("hidden")&&(n.contains(e)||n.classList.add("hidden")),!d&&o&&!o.classList.contains("hidden")&&(o.contains(e)||o.classList.add("hidden"))});window.toggleThemeDropdown=i;window.toggleUserMenu=a;window.changeThemeTo=c;window.logout=l;})();
|
||||
// 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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+230
-18
@@ -1,55 +1,267 @@
|
||||
(()=>{var i=null,x=300,v=2;function b(){let e=document.getElementById("header-search");if(!e){console.warn("Search input not found");return}e.addEventListener("input",E),e.addEventListener("keydown",I),e.addEventListener("focus",()=>{e.value.length>=v&&y(e.value)}),document.addEventListener("click",r=>{let t=document.getElementById("search-results"),s=document.getElementById("header-search");t&&!t.contains(r.target)&&r.target!==s&&c()})}function E(e){let t=e.target.value.trim();if(i&&clearTimeout(i),t.length<v){c();return}i=setTimeout(()=>{y(t)},x)}function I(e){let r=document.getElementById("search-results");if(!r||r.classList.contains("hidden"))return;let t=r.querySelectorAll(".search-result-item"),s=parseInt(r.dataset.selectedIndex||"-1");if(e.key==="ArrowDown"){e.preventDefault();let n=Math.min(s+1,t.length-1);h(t,n)}else if(e.key==="ArrowUp"){e.preventDefault();let n=Math.max(s-1,-1);h(t,n)}else if(e.key==="Enter"){if(e.preventDefault(),s>=0&&t[s]){let n=t[s].querySelector("a");n&&n.click()}}else e.key==="Escape"&&c()}function h(e,r){e.forEach((s,n)=>{n===r?s.classList.add("bg-opacity-80"):s.classList.remove("bg-opacity-80")});let t=document.getElementById("search-results");t&&(t.dataset.selectedIndex=r.toString())}function y(e){let r=localStorage.getItem("token");if(!r){console.warn("No authentication token found");return}k(),fetch(`/api/media-items/search?q=${encodeURIComponent(e)}`,{headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"}}).then(t=>t.status===404?{error:"no results found",results:[]}:t.json()).then(t=>{t&&"error"in t&&t.error==="no results found"?d(e):Array.isArray(t)&&t.length>0?L(t,e):(Array.isArray(t),d(e))}).catch(t=>{console.error("Search error:",t),S()})}function k(){l();let e=document.getElementById("search-results");e&&(e.innerHTML=`
|
||||
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>
|
||||
`,e.classList.remove("hidden"))}function L(e,r){l();let t=document.getElementById("search-results");if(!t)return;t.dataset.selectedIndex="-1";let s={ebooks:"\u{1F4DA}",comics:"\u{1F4D6}",manga:"\u{1F5FE}"},n=`
|
||||
`;
|
||||
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)">
|
||||
${e.length} result${e.length!==1?"s":""} for "${a(r)}"
|
||||
${results.length} result${results.length !== 1 ? "s" : ""} for "${searchEscapeHtml(query)}"
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
`;e.forEach((o,p)=>{let f=s[o.library_type_name]||"\u{1F4C1}",g=m(o.title,r),u=o.author?m(o.author,r):"";n+=`
|
||||
`;
|
||||
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="${p}">
|
||||
data-index="${index}">
|
||||
<a href="/bookshelf"
|
||||
class="block"
|
||||
onclick="window.selectLibraryAndBook('${o.library_id}', '${o.id}')">
|
||||
onclick="window.selectLibraryAndBook('${item.library_id}', '${item.id}')">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="text-2xl">${f}</div>
|
||||
<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)">
|
||||
${g}
|
||||
${titleHtml}
|
||||
</h4>
|
||||
${u?`<p class="text-xs truncate" style="color: var(--text-secondary)">${u}</p>`:""}
|
||||
${authorHtml ? `<p class="text-xs truncate" style="color: var(--text-secondary)">${authorHtml}</p>` : ""}
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">
|
||||
${a(o.library_name)}
|
||||
${searchEscapeHtml(item.library_name)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`}),n+=`
|
||||
`;
|
||||
});
|
||||
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)">\u2191\u2193</kbd> to navigate,
|
||||
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>
|
||||
`,t.innerHTML=n,t.classList.remove("hidden")}function d(e){l();let r=document.getElementById("search-results");r&&(r.innerHTML=`
|
||||
`;
|
||||
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">\u{1F50D}</div>
|
||||
<p class="text-sm" style="color: var(--text-primary)">No results found for "${a(e)}"</p>
|
||||
<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>
|
||||
`,r.classList.remove("hidden"))}function S(){l();let e=document.getElementById("search-results");e&&(e.innerHTML=`
|
||||
`;
|
||||
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">\u26A0\uFE0F</div>
|
||||
<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>
|
||||
`,e.classList.remove("hidden"))}function c(){let e=document.getElementById("search-results");e&&e.classList.add("hidden")}function l(){let e=document.getElementById("search-results");if(!e){e=document.createElement("div"),e.id="search-results",e.className="hidden absolute z-50 w-full max-w-2xl mt-2 rounded-lg shadow-lg border",e.style.cssText="background-color: var(--bg-secondary); border-color: var(--border)";let r=document.getElementById("header-search");if(r){let t=r.closest(".relative");t&&t.appendChild(e)}}}function m(e,r){if(!e)return"";let t=r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),s=new RegExp(`(${t})`,"gi");return a(e).replace(s,'<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>')}function a(e){let r=document.createElement("div");return r.textContent=e,r.innerHTML}function w(e,r){localStorage.setItem("selectedLibrary",e),localStorage.setItem("selectedBook",r),c()}document.addEventListener("DOMContentLoaded",b);window.selectLibraryAndBook=w;})();
|
||||
`;
|
||||
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