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
+13 -15
View File
@@ -1,5 +1,3 @@
import type { BookInfo } from './types/api';
interface FilterField {
id: string;
label: string;
@@ -176,7 +174,7 @@ const FILTER_FIELDS: FilterField[] = [
let ruleCounter = 0;
let selectedBooks: Map<string, BookInfo> = new Map();
let searchTimeout: number | null = null;
let customSectionTimeout: number | null = null;
function initCustomSectionBuilder(): void {
const addRuleBtn = document.getElementById('add-rule-btn');
@@ -312,10 +310,10 @@ function removeFilterRule(ruleId: string): void {
}
function onBookSearchInput(): void {
if (searchTimeout) {
clearTimeout(searchTimeout);
if (customSectionTimeout) {
clearTimeout(customSectionTimeout);
}
searchTimeout = window.setTimeout(() => {
customSectionTimeout = window.setTimeout(() => {
searchBooks();
}, 300);
}
@@ -363,13 +361,13 @@ function displaySearchResults(books: BookInfo[]): void {
resultsContainer.innerHTML = books.map(book => `
<div class="flex items-center gap-2 p-2 hover:bg-gray-700 rounded cursor-pointer"
data-book-id="${book.media_item_id}"
onclick="addBookToSelection('${book.media_item_id}', '${escapeHtml(book.title)}', '${escapeHtml(book.author)}')">
onclick="addBookToSelection('${book.media_item_id}', '${builderEscapeHtml(book.title)}', '${builderEscapeHtml(book.author)}')">
<img src="${book.cover_image_path || '/static/placeholder-book.svg'}"
alt="${escapeHtml(book.title)}"
alt="${builderEscapeHtml(book.title)}"
class="w-10 h-15 object-cover rounded">
<div class="flex-1">
<p class="text-sm font-medium" style="color: var(--text-primary);">${escapeHtml(book.title)}</p>
<p class="text-xs" style="color: var(--text-secondary);">${escapeHtml(book.author)}</p>
<p class="text-sm font-medium" style="color: var(--text-primary);">${builderEscapeHtml(book.title)}</p>
<p class="text-xs" style="color: var(--text-secondary);">${builderEscapeHtml(book.author)}</p>
</div>
<button type="button" class="text-green-500 hover:text-green-700 text-xl">+</button>
</div>
@@ -412,7 +410,7 @@ function updateSelectedBooksDisplay(): void {
container.innerHTML = Array.from(selectedBooks.values()).map(book => `
<div class="inline-flex items-center gap-2 px-3 py-1 m-1 rounded-full text-sm"
style="background-color: var(--accent);">
<span>${escapeHtml(book.title)}</span>
<span>${builderEscapeHtml(book.title)}</span>
<button type="button" onclick="removeBookFromSelection('${book.media_item_id}')"
class="hover:opacity-70">×</button>
</div>
@@ -495,13 +493,13 @@ function displayPreview(items: BookInfo[]): void {
<div class="flex-shrink-0 w-32">
<div class="aspect-[2/3] rounded-lg overflow-hidden shadow-lg mb-2">
<img src="${item.cover_image_path || '/static/placeholder-book.svg'}"
alt="${escapeHtml(item.title)}"
alt="${builderEscapeHtml(item.title)}"
class="w-full h-full object-cover">
</div>
<h3 class="text-sm font-semibold line-clamp-2" style="color: var(--text-primary);">
${escapeHtml(item.title)}
${builderEscapeHtml(item.title)}
</h3>
${item.author ? `<p class="text-xs line-clamp-1" style="color: var(--text-secondary);">${escapeHtml(item.author)}</p>` : ''}
${item.author ? `<p class="text-xs line-clamp-1" style="color: var(--text-secondary);">${builderEscapeHtml(item.author)}</p>` : ''}
</div>
`).join('')}
</div>
@@ -560,7 +558,7 @@ async function saveCustomSection(event: Event): Promise<void> {
}
}
function escapeHtml(text: string): string {
function builderEscapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;