Remove redundant <script src="/static/main.js" defer></script> tags from 17+ templates that include the @Header component, eliminating duplicate script loading that was causing Alpine.js to initialize twice per page load. The header.templ component now serves as the single source of truth for main.js inclusion, following the DRY principle and ensuring consistent script loading across all pages that use the header navigation. Additionally, add type="button" attribute to all buttons in header navigation to prevent default form submission behavior when buttons are clicked. Changes: - Remove main.js script tag from templates using @Header component - Keep main.js in header.templ (line 279) as universal inclusion point - Preserve main.js in special pages: index.templ, login.templ, register.templ (these don't use @Header and are standalone entry points) - Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons to prevent unwanted form submissions or page navigation Benefits: - Eliminates Alpine.js double-initialization bug - Reduces HTTP requests (one script load instead of two) - Improves maintainability (add header, get scripts automatically) - Fixes broken @click handlers on collections, devices, and other pages - Prevents buttons from triggering default form submission behavior Technical notes: - Templates affected: admin, analytics, bookshelf, collection_rules, collections, conflicts, custom_section, dashboard, devices, docs, library, profile, progress, queue, unlinked_books - No changes to entry pages (index, login, register) which don't use @Header - HTMX script remains in individual templates (stateless, no double-load issue) - All interactive buttons in header now explicitly marked type="button" to prevent default browser form submission behavior Related to: previous commit fixing Vite code-splitting
102 lines
3.9 KiB
Templ
102 lines
3.9 KiB
Templ
package templates
|
|
|
|
import "bookhoard/internal/handlers"
|
|
|
|
templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Reading Progress - Bookhoard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body class="theme-{ user.Theme }">
|
|
@Header(user, "/progress")
|
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Reading Progress</h1>
|
|
<p style="color: var(--text-secondary)">Track your reading progress across all devices</p>
|
|
</div>
|
|
<div id="progress-container" class="space-y-6">
|
|
if len(progressData) == 0 {
|
|
<div class="text-center py-16" style="color: var(--text-secondary)">
|
|
<div class="text-6xl mb-4">📖</div>
|
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Progress Yet</h3>
|
|
<p>Start reading a book to track your progress</p>
|
|
</div>
|
|
}
|
|
for _, item := range progressData {
|
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex gap-6">
|
|
<div class="flex-shrink-0">
|
|
<img
|
|
src="{ item.CoverImagePath }"
|
|
alt="Cover"
|
|
class="w-24 h-32 object-cover rounded"
|
|
onerror="this.src='/static/placeholder-book.svg'"
|
|
/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<div>
|
|
<h3 class="font-semibold text-lg" style="color: var(--text-primary)">{ item.Title }</h3>
|
|
if item.Author != "" {
|
|
<p class="text-sm" style="color: var(--text-secondary)">by { item.Author }</p>
|
|
}
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="text-2xl font-bold" style="color: var(--accent)">{ item.ProgressPercentage }%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<div class="w-full bg-gray-700 rounded-full h-3">
|
|
<div class="h-3 rounded-full transition-all" style="width: { item.ProgressPercentage }%; background-color: var(--accent);"></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
|
<div>
|
|
<p style="color: var(--text-secondary)">Current Position</p>
|
|
<p style="color: var(--text-primary)">
|
|
{ item.CurrentPage } / { item.TotalPages }
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p style="color: var(--text-secondary)">Last Updated</p>
|
|
<p style="color: var(--text-primary)">{ item.LastUpdated }</p>
|
|
</div>
|
|
<div>
|
|
<p style="color: var(--text-secondary)">Synced From</p>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xl">{ item.DeviceIcon }</span>
|
|
<span style="color: var(--text-primary)">{ item.DeviceName }</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
if item.DeviceIcon != "" {
|
|
<div class="mt-3 pt-3 border-t" style="border-color: var(--border);">
|
|
<div class="flex items-center gap-2 text-xs" style="color: var(--text-secondary);">
|
|
<span>🔄</span>
|
|
<span>Last sync from <strong>{ item.DeviceName }</strong></span>
|
|
<span>({ item.DeviceType })</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
if item.EpubCFI != "" {
|
|
<div class="mt-2 text-xs" style="color: var(--text-secondary);">
|
|
<span>📍</span>
|
|
<span>CFI: { item.EpubCFI }</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
@ErrorToast(errorMessage)
|
|
</body>
|
|
</html>
|
|
}
|