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.
310 lines
10 KiB
Templ
310 lines
10 KiB
Templ
package templates
|
||
|
||
import (
|
||
"bookhoard/internal/handlers"
|
||
"fmt"
|
||
)
|
||
|
||
templ Dashboard(user User, sections []handlers.SectionData, libData []LibraryData, currentLibraryID string, errorMessage string) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Dashboard - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<script src="/static/toast.js"></script>
|
||
<script src="/static/api.js"></script>
|
||
<script src="/static/events.js"></script>
|
||
<script src="/static/dashboard.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
</head>
|
||
<body class="theme-{ user.Theme }">
|
||
@Header(user, "/dashboard")
|
||
<!-- Sticky Library Selector -->
|
||
<div class="sticky top-0 z-40 bg-opacity-95 backdrop-blur border-b" style="background-color: var(--bg-primary);">
|
||
<div class="w-full px-4 py-3 flex items-center justify-between">
|
||
<div class="flex items-center gap-4">
|
||
<label class="text-sm font-medium" style="color: var(--text-secondary)">Library:</label>
|
||
<select
|
||
id="library-select"
|
||
name="library_id"
|
||
class="px-4 py-2 rounded-lg border focus:ring-2 focus:ring-blue-500"
|
||
style="background-color: var(--bg-secondary); color: var(--text-primary);"
|
||
data-action="switch-library"
|
||
>
|
||
for _, lib := range libData {
|
||
if lib.ID == currentLibraryID {
|
||
<option value={ lib.ID } selected>{ lib.Name }</option>
|
||
} else {
|
||
<option value={ lib.ID }>{ lib.Name }</option>
|
||
}
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
data-action="open-dashboard-settings"
|
||
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||
style="background-color: var(--bg-secondary);"
|
||
title="Customize Dashboard"
|
||
>
|
||
⚙️
|
||
</button>
|
||
<button
|
||
data-action="reload-page"
|
||
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
||
style="background-color: var(--bg-secondary);"
|
||
title="Refresh"
|
||
>
|
||
🔄
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
id="loading-spinner"
|
||
class="hidden fixed inset-0 bg-opacity-50 flex items-center justify-center z-50"
|
||
style="background-color: var(--bg-primary);"
|
||
>
|
||
<div class="animate-spin rounded-full h-12 w-12 border-b-2" style="border-color: var(--accent);"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Collections Container -->
|
||
<main id="collections-container" class="w-full px-4 py-8">
|
||
for _, section := range sections {
|
||
@CollectionCarousel(section)
|
||
}
|
||
</main>
|
||
<!-- Dashboard Settings Modal -->
|
||
@DashboardSettingsModal(sections)
|
||
@ErrorToast(errorMessage)
|
||
<script src="/static/woodPanelingInit.js"></script>
|
||
</body>
|
||
</html>
|
||
}
|
||
|
||
templ CollectionCarousel(section handlers.SectionData) {
|
||
<div
|
||
class="dashboard-collection mb-8"
|
||
data-collection-id={ section.ID }
|
||
data-is-system={ section.IsSystem }
|
||
>
|
||
<!-- Collection Header -->
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-2xl">{ section.Icon }</span>
|
||
<div>
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">{ section.Title }</h2>
|
||
if section.Description != "" {
|
||
<p class="text-sm" style="color: var(--text-secondary)">{ section.Description }</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
if section.ViewAllURL != "" {
|
||
<a
|
||
href={ section.ViewAllURL }
|
||
class="text-sm font-medium hover:underline transition-colors"
|
||
style="color: var(--accent);"
|
||
>
|
||
View All →
|
||
</a>
|
||
}
|
||
</div>
|
||
<!-- Carousel -->
|
||
<div class="carousel-container relative group">
|
||
<button
|
||
class="carousel-nav-left absolute left-0 top-1/2 -translate-y-1/2 z-10
|
||
w-12 h-full bg-gradient-to-r from-gray-900 to-transparent
|
||
flex items-center justify-start opacity-0 group-hover:opacity-100
|
||
transition-opacity duration-200"
|
||
data-action="scroll-carousel"
|
||
data-collection-id={ section.ID }
|
||
data-direction="-1"
|
||
aria-label="Scroll left"
|
||
>
|
||
<span class="text-3xl pl-2" style="color: var(--text-primary);">‹</span>
|
||
</button>
|
||
<div
|
||
id={ "carousel-track-" + section.ID }
|
||
class="carousel-track flex gap-4 overflow-x-auto
|
||
scroll-smooth snap-x snap-mandatory
|
||
px-12 pb-4"
|
||
style="scrollbar-width: none; -ms-overflow-style: none;"
|
||
>
|
||
for _, item := range section.Items {
|
||
@BookCard(item)
|
||
}
|
||
if len(section.Items) == 0 {
|
||
<div class="text-center py-8 w-full" style="color: var(--text-secondary);">
|
||
<p>No items in this collection</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
<button
|
||
class="carousel-nav-right absolute right-0 top-1/2 -translate-y-1/2 z-10
|
||
w-12 h-full bg-gradient-to-l from-gray-900 to-transparent
|
||
flex items-center justify-end opacity-0 group-hover:opacity-100
|
||
transition-opacity duration-200"
|
||
data-action="scroll-carousel"
|
||
data-collection-id={ section.ID }
|
||
data-direction="1"
|
||
aria-label="Scroll right"
|
||
>
|
||
<span class="text-3xl pr-2" style="color: var(--text-primary);">›</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ BookCard(item handlers.BookInfo) {
|
||
<div
|
||
class="book-card flex-shrink-0 w-32 snap-start cursor-pointer
|
||
transition-transform duration-200 hover:scale-105"
|
||
data-action="view-book"
|
||
data-book-id={ item.MediaItemID }
|
||
tabindex="0"
|
||
role="button"
|
||
aria-label={ "View " + item.Title }
|
||
>
|
||
<div
|
||
class="aspect-[2/3] rounded-lg overflow-hidden shadow-lg mb-2
|
||
bg-gradient-to-br from-gray-700 to-gray-900"
|
||
>
|
||
if item.CoverImagePath != "" {
|
||
<img
|
||
src={ item.CoverImagePath }
|
||
alt={ item.Title }
|
||
class="w-full h-full object-cover"
|
||
loading="lazy"
|
||
onerror="this.src='/static/placeholder-book.svg'"
|
||
/>
|
||
} else {
|
||
<img
|
||
src="/static/placeholder-book.svg"
|
||
alt={ item.Title }
|
||
class="w-full h-full object-cover"
|
||
/>
|
||
}
|
||
</div>
|
||
<h3 class="font-semibold text-sm line-clamp-2" style="color: var(--text-primary)">
|
||
{ item.Title }
|
||
</h3>
|
||
if item.Author != "" {
|
||
<p class="text-xs line-clamp-1" style="color: var(--text-secondary)">
|
||
{ item.Author }
|
||
</p>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
templ DashboardSettingsModal(sections []handlers.SectionData) {
|
||
<div
|
||
id="dashboard-settings-modal"
|
||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||
style="background-color: rgba(0, 0, 0, 0.7);"
|
||
>
|
||
<div
|
||
class="rounded-lg p-6 w-full max-w-2xl mx-4 shadow-2xl"
|
||
style="background-color: var(--bg-secondary);"
|
||
>
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Customize Dashboard</h2>
|
||
<button
|
||
data-action="close-dashboard-settings"
|
||
class="p-2 hover:bg-gray-700 rounded transition-colors"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<p class="text-sm mb-4" style="color: var(--text-secondary);">
|
||
Drag to reorder collections, toggle visibility with the switch.
|
||
</p>
|
||
<!-- Draggable Collection List -->
|
||
<div id="collection-list" class="space-y-2 mb-6">
|
||
for _, section := range sections {
|
||
<div
|
||
class="collection-item flex items-center justify-between p-3 rounded border
|
||
cursor-move select-none"
|
||
data-collection-id={ section.ID }
|
||
data-is-system={ fmt.Sprintf("%v", section.IsSystem) }
|
||
draggable="true"
|
||
style="background-color: var(--bg-primary); border-color: var(--border);"
|
||
>
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-xl" style="color: var(--text-secondary);">☰</span>
|
||
<span class="text-xl">{ section.Icon }</span>
|
||
<div>
|
||
<span class="font-medium" style="color: var(--text-primary);">{ section.Title }</span>
|
||
if section.IsSystem {
|
||
<span class="text-xs ml-2 px-2 py-1 rounded" style="background-color: var(--accent);">System</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
if section.IsSystem {
|
||
<button
|
||
data-action="restore-system-collection"
|
||
data-collection-name={ section.ID }
|
||
class="text-xs px-3 py-1 rounded border hover:opacity-80 transition-opacity"
|
||
style="border-color: var(--border); color: var(--text-secondary);"
|
||
title="Restore { section.Title } to defaults"
|
||
>
|
||
Restore
|
||
</button>
|
||
}
|
||
<label class="relative inline-flex items-center cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
class="sr-only peer"
|
||
checked
|
||
data-action="toggle-collection-visibility"
|
||
data-collection-id={ section.ID }
|
||
/>
|
||
<div
|
||
class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer
|
||
peer-checked:after:translate-x-full peer-checked:after:border-white
|
||
after:content-[''] after:absolute after:top-[2px] after:left-[2px]
|
||
after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all
|
||
peer-checked:bg-blue-600"
|
||
></div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
<!-- Items Per Section Slider -->
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">
|
||
Items per Collection: <span id="items-count-display" class="font-bold">20</span>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="10"
|
||
max="50"
|
||
step="5"
|
||
value="20"
|
||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||
data-action="update-items-count"
|
||
target="items-count-display"
|
||
/>
|
||
</div>
|
||
<div class="flex justify-end gap-3">
|
||
<button
|
||
data-action="close-dashboard-settings"
|
||
class="px-4 py-2 rounded-lg border hover:bg-gray-700 transition-colors"
|
||
style="border-color: var(--border); color: var(--text-primary);"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
data-action="save-dashboard-settings"
|
||
class="px-4 py-2 rounded-lg text-white font-medium hover:opacity-90 transition-opacity"
|
||
style="background-color: var(--accent);"
|
||
>
|
||
Save Changes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|