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
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 192" width="128" height="192">
<defs>
<linearGradient id="bookGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4B5563"/>
<stop offset="100%" style="stop-color:#1F2937"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="128" height="192" rx="8" fill="url(#bookGrad)"/>
<rect x="8" y="8" width="112" height="176" rx="4" fill="none" stroke="#6B7280" stroke-width="2"/>
<text x="64" y="100" text-anchor="middle" fill="#9CA3AF" font-family="sans-serif" font-size="40">📚</text>
</svg>

After

Width:  |  Height:  |  Size: 602 B

+9 -10
View File
@@ -1,6 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
let searchTimeout = null;
let searchInputTimeout = null;
const SEARCH_DEBOUNCE_MS = 300;
const SEARCH_MIN_CHARS = 2;
function initializeSearch() {
@@ -27,14 +26,14 @@ function initializeSearch() {
function handleSearchInput(e) {
const target = e.target;
const query = target.value.trim();
if (searchTimeout) {
clearTimeout(searchTimeout);
if (searchInputTimeout) {
clearTimeout(searchInputTimeout);
}
if (query.length < SEARCH_MIN_CHARS) {
hideSearchResults();
return;
}
searchTimeout = setTimeout(() => {
searchInputTimeout = setTimeout(() => {
performSearch(query);
}, SEARCH_DEBOUNCE_MS);
}
@@ -150,7 +149,7 @@ function showSearchResults(results, query) {
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">
@@ -174,7 +173,7 @@ function showSearchResults(results, query) {
</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>
@@ -202,7 +201,7 @@ function showNoResults(query) {
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>
`;
@@ -249,9 +248,9 @@ function highlightMatch(text, query) {
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) {
function searchEscapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;