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
281 lines
13 KiB
Templ
281 lines
13 KiB
Templ
package templates
|
|
|
|
templ Header(user User, currentPath string) {
|
|
<nav x-data="header" x-init="initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators()" class="border-b header-nav" style="border-color: var(--border); background-color: var(--bg-secondary);">
|
|
<div x-data="{}" x-init="console.log('Alpine is working!', $el)"></div>
|
|
<div class="w-full px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<!-- Left: App Title & Navigation -->
|
|
<div class="flex items-center space-x-6">
|
|
<a href="/dashboard" class="text-xl font-bold hover:opacity-80 transition-opacity" style="color: var(--text-primary); text-decoration: none;">
|
|
📚 Bookhoard
|
|
</a>
|
|
<div class="hidden md:flex items-center space-x-4">
|
|
<a href="/dashboard" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
|
Library
|
|
</a>
|
|
<a href="/collections" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
|
Collections
|
|
</a>
|
|
<a href="/progress" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
|
Progress
|
|
</a>
|
|
<a href="/devices" class="text-sm hover:opacity-80 transition-opacity" style="color: var(--text-secondary); text-decoration: none;">
|
|
Devices
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<!-- Center: Search Box -->
|
|
<div class="flex-1 max-w-2xl mx-8">
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
id="header-search"
|
|
placeholder="Search your library..."
|
|
class="w-full px-4 py-2 pl-10 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
autocomplete="off"
|
|
/>
|
|
<svg class="absolute left-3 top-2.5 h-5 w-5" style="color: var(--text-secondary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<!-- Right: Theme Switcher & User Menu -->
|
|
<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }" class="flex items-center space-x-4">
|
|
<!-- Theme Switcher -->
|
|
<div class="relative">
|
|
<button
|
|
@click="themeDropdownOpen = !themeDropdownOpen"
|
|
type="button"
|
|
class="p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
|
style="background-color: var(--bg-primary);"
|
|
>
|
|
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
|
|
</svg>
|
|
</button>
|
|
<div
|
|
x-show="themeDropdownOpen"
|
|
@click.outside="themeDropdownOpen = false"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50"
|
|
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
|
>
|
|
<div class="p-4">
|
|
<h3 class="text-sm font-semibold mb-3" style="color: var(--text-primary)">Select Theme</h3>
|
|
<div class="space-y-2">
|
|
<button
|
|
@click="changeTheme('tokyo-night'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #7aa2f7;"></span>
|
|
Tokyo Night
|
|
</button>
|
|
<button
|
|
@click="changeTheme('dracula'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #bd93f9;"></span>
|
|
Dracula
|
|
</button>
|
|
<button
|
|
@click="changeTheme('nord'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #88c0d0;"></span>
|
|
Nord
|
|
</button>
|
|
<button
|
|
@click="changeTheme('solarized-dark'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #2aa198;"></span>
|
|
Solarized Dark
|
|
</button>
|
|
<button
|
|
@click="changeTheme('monokai'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #a6e22e;"></span>
|
|
Monokai
|
|
</button>
|
|
<button
|
|
@click="changeTheme('one-dark-pro'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #61dafb;"></span>
|
|
One Dark Pro
|
|
</button>
|
|
<button
|
|
@click="changeTheme('material-dark'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary); background-color: var(--bg-primary);"
|
|
>
|
|
<span class="inline-block w-4 h-4 rounded mr-2" style="background-color: #80cbc4;"></span>
|
|
Material Dark
|
|
</button>
|
|
<div class="border-t pt-2 mt-2" style="border-color: var(--border);">
|
|
<p class="text-xs mb-2" style="color: var(--text-secondary)">Bookshelf Background</p>
|
|
<button
|
|
@click="changeWoodPaneling('none'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary);"
|
|
data-wood="none"
|
|
>
|
|
None
|
|
</button>
|
|
<button
|
|
@click="changeWoodPaneling('wood-light'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary);"
|
|
data-wood="wood-light"
|
|
>
|
|
<span
|
|
class="inline-block w-4 h-4 rounded mr-2"
|
|
style="background: url('/static/textures/wood-light.png'); background-size: cover;"
|
|
></span>
|
|
Wood Light
|
|
</button>
|
|
<button
|
|
@click="changeWoodPaneling('wood-dark'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary);"
|
|
data-wood="wood-dark"
|
|
>
|
|
<span
|
|
class="inline-block w-4 h-4 rounded mr-2"
|
|
style="background: url('/static/textures/wood-dark.png'); background-size: cover;"
|
|
></span>
|
|
Wood Dark
|
|
</button>
|
|
<button
|
|
@click="changeWoodPaneling('wood-mahogany'); themeDropdownOpen = false"
|
|
type="button"
|
|
class="wood-paneling-btn w-full text-left px-3 py-2 rounded hover:opacity-80 transition-opacity"
|
|
style="color: var(--text-primary);"
|
|
data-wood="wood-mahogany"
|
|
>
|
|
<span
|
|
class="inline-block w-4 h-4 rounded mr-2"
|
|
style="background: url('/static/textures/wood-mahogany.png'); background-size: cover;"
|
|
></span>
|
|
Wood Mahogany
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- User Icon with Dropdown -->
|
|
<div class="relative">
|
|
if user.ID != "" {
|
|
<!-- LOGGED IN: Show user menu with logout -->
|
|
<button
|
|
@click="userMenuOpen = !userMenuOpen"
|
|
type="button"
|
|
class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
|
style="background-color: var(--bg-primary);"
|
|
>
|
|
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
|
</svg>
|
|
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">{ user.Username }</span>
|
|
</button>
|
|
<div
|
|
x-show="userMenuOpen"
|
|
@click.outside="userMenuOpen = false"
|
|
x-transition
|
|
class="absolute right-0 mt-2 w-48 rounded-lg shadow-lg z-50"
|
|
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
|
>
|
|
<div class="py-1">
|
|
<a href="/profile" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
|
Profile
|
|
</a>
|
|
if user.Role == "admin" {
|
|
<a href="/admin" class="block px-4 py-2 text-sm hover:opacity-80" style="color: var(--text-primary); background-color: var(--bg-secondary); text-decoration: none;">
|
|
Admin Panel
|
|
</a>
|
|
}
|
|
<div class="border-t my-1" style="border-color: var(--border);"></div>
|
|
<button
|
|
@click="logout(); userMenuOpen = false"
|
|
type="button"
|
|
class="block w-full text-left px-4 py-2 text-sm hover:opacity-80"
|
|
style="color: var(--text-primary); background-color: var(--bg-secondary);"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
} else {
|
|
<!-- LOGGED OUT: Show login form -->
|
|
<button
|
|
@click="userMenuOpen = !userMenuOpen"
|
|
type="button"
|
|
class="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-700 transition-colors"
|
|
style="background-color: var(--bg-primary);"
|
|
>
|
|
<svg class="h-6 w-6" style="color: var(--text-primary)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
|
</svg>
|
|
<span class="text-sm hidden sm:block" style="color: var(--text-primary)">Login</span>
|
|
</button>
|
|
<div
|
|
x-show="userMenuOpen"
|
|
@click.outside="userMenuOpen = false"
|
|
x-transition
|
|
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50 p-4"
|
|
style="background-color: var(--bg-secondary); border: 1px solid var(--border); display: none;"
|
|
>
|
|
<form hx-post="/api/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-3">
|
|
<input type="hidden" name="redirect" value="{ currentPath }"/>
|
|
<div>
|
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Email or Username</label>
|
|
<input type="text" name="login" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm mb-1" style="color: var(--text-secondary)">Password</label>
|
|
<input type="password" name="password" class="w-full px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" required/>
|
|
</div>
|
|
<button type="submit" class="w-full py-2 rounded text-sm" style="background-color: var(--accent); color: white;">
|
|
Sign In
|
|
</button>
|
|
</form>
|
|
<div id="login-result"></div>
|
|
<div class="border-t my-2" style="border-color: var(--border);"></div>
|
|
<a href="/register" class="block text-center text-sm hover:opacity-80" style="color: var(--text-secondary); text-decoration: none;">
|
|
Don't have an account? Sign Up
|
|
</a>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<script src="/static/main.js" defer></script>
|
|
}
|