Applies consistent code formatting to template source files: Changes: - Fixed inconsistent indentation in admin_library.templ (tabs vs spaces) - Standardized whitespace alignment across multiple template files - Ensured generated Go files match reformatted sources Templates Updated: - admin_library.templ: Indentation fixes for AdminSidebar component - analytics.templ: Whitespace normalization - collections.templ: Whitespace normalization - conflicts.templ: Whitespace normalization - dashboard.templ: Whitespace normalization Generated Go Files: - All corresponding *_templ.go files regenerated to match These are cosmetic formatting changes only - no functional changes. Alpine.js integration, template logic, and application behavior unchanged.
307 lines
10 KiB
Templ
307 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/main.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)
|
||
</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)_40%,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>
|
||
}
|