Add dashboard redesign, custom section builder, and enhanced search functionality

Features:
- Complete dashboard redesign with improved UI components and layout
- Implement custom section builder for personalized book organization
- Add new events tracking system for user interactions
- Enhance search functionality with better static search.js
- Update TypeScript type definitions for API responses

Backend:
- Update Go dependencies in go.mod
- Add new frontend routes in router

Templates:
- Update admin and dashboard templates with new components

Frontend:
- Refactor analytics, collections, conflicts, and queue modules
- Add new documentation features in docs.ts
- Implement linking between books and collections
- Add toast notifications for user feedback
- Include placeholder book SVG asset

This commit consolidates multiple feature additions and improvements
across the entire stack including backend, templates, and frontend.
This commit is contained in:
2026-02-25 16:56:10 -05:00
parent 5864710e4f
commit a8920a8f6c
19 changed files with 1718 additions and 533 deletions
+9 -11
View File
@@ -1,6 +1,4 @@
import type { MediaItemSummary } from './types/api';
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
let searchInputTimeout: ReturnType<typeof setTimeout> | null = null;
const SEARCH_DEBOUNCE_MS = 300;
const SEARCH_MIN_CHARS = 2;
@@ -33,8 +31,8 @@ function handleSearchInput(e: Event): void {
const target = e.target as HTMLInputElement;
const query = target.value.trim();
if (searchTimeout) {
clearTimeout(searchTimeout);
if (searchInputTimeout) {
clearTimeout(searchInputTimeout);
}
if (query.length < SEARCH_MIN_CHARS) {
@@ -42,7 +40,7 @@ function handleSearchInput(e: Event): void {
return;
}
searchTimeout = setTimeout(() => {
searchInputTimeout = setTimeout(() => {
performSearch(query);
}, SEARCH_DEBOUNCE_MS);
}
@@ -164,7 +162,7 @@ function showSearchResults(results: MediaItemSummary[], query: string): void {
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 "${escapeHtml(query)}"
${results.length} result${results.length !== 1 ? 's' : ''} for "${searchEscapeHtml(query)}"
</p>
</div>
<div class="max-h-96 overflow-y-auto">
@@ -190,7 +188,7 @@ function showSearchResults(results: MediaItemSummary[], query: string): void {
</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)">
${escapeHtml(item.library_name)}
${searchEscapeHtml(item.library_name)}
</p>
</div>
</div>
@@ -221,7 +219,7 @@ function showNoResults(query: string): void {
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 "${escapeHtml(query)}"</p>
<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>
`;
@@ -272,10 +270,10 @@ function highlightMatch(text: string, query: string): string {
if (!text) return '';
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return escapeHtml(text).replace(regex, '<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>');
return searchEscapeHtml(text).replace(regex, '<mark style="background-color: var(--accent); color: var(--bg-primary); padding: 0 2px; border-radius: 2px;">$1</mark>');
}
function escapeHtml(text: string): string {
function searchEscapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;