Fix multiple issues with dashboard customization modal and slider not working correctly per library. Frontend changes in web/src/dashboard.ts: - Fix openDashboardSettings() to use current library ID - Add library_id parameter to dashboard preferences API call - Show toast error message on API failure instead of opening modal - Prevent opening modal with stale/inaccurate data - Fix slider query parameter mismatch - Change from 'libraryId' to 'library_id' to match backend API - Fix DOM query from collectionList.querySelector to document.querySelector - Ensure slider targets correct input element - Fix saveDashboardSettings() to refresh current library - Fetch current library data before saving preferences - Use library_id from current library, not from URL - Show toast error message on save failure - Keep modal open on error for user to retry - Add localStorage persistence for selected library - Store selectedLibrary in localStorage after switching - Enables persistence across page refreshes - Improve switchLibrary() with fade transitions - Add fade-out (150ms) before data fetch - Add fade-in (300ms) after rendering new library - Provide smooth visual feedback during library switches - Apply preferences dynamically to modal - Use applyPreferencesToModal() to update slider and toggles - Ensure modal reflects current library's settings Backend changes in internal/router/dashboard.go: - Update GetDashboardPreferences to use library_id query parameter - Matches frontend API call parameter naming Template changes in templates/dashboard.templ: - Remove duplicate renderDashboardCollections() inline script - Functionality now handled by dashboard.ts These fixes ensure that: 1. Dashboard settings work correctly per library 2. Slider reflects and updates the correct library's item limit 3. Toggles show accurate visibility state for each library 4. Library switches provide smooth visual feedback 5. Errors are properly surfaced to users via toast messages
311 lines
10 KiB
Templ
311 lines
10 KiB
Templ
package templates
|
||
|
||
import (
|
||
"bookhoard/internal/handlers"
|
||
"fmt"
|
||
)
|
||
|
||
templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, 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);"
|
||
>
|
||
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(allSections, hiddenCollections, itemsPerSection)
|
||
@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-36 rounded-lg overflow-hidden 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] overflow-hidden shadow-lg
|
||
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>
|
||
<div class="book-card-text px-2 py-1 bg-[color-mix(in_srgb,var(--wood-border)_60%,transparent)]">
|
||
<h3 class="font-semibold text-base line-clamp-2" style="color: var(--text-primary)">
|
||
{ item.Title }
|
||
</h3>
|
||
if item.Author != "" {
|
||
<p class="text-sm line-clamp-1" style="color: var(--text-secondary)">
|
||
{ item.Author }
|
||
</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections []string, itemsPerSection int) {
|
||
<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"
|
||
if !ContainsString(hiddenCollections, section.ID) {
|
||
checked
|
||
}
|
||
/>
|
||
<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">{ itemsPerSection }</span>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="10"
|
||
max="50"
|
||
step="5"
|
||
value={ itemsPerSection }
|
||
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||
data-input-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>
|
||
}
|