Files
bookhoard/templates/setup.templ
T
John O'Keefe f1a6de7cd7 ui: fix admin/docs theme flash + persist sidebar open/closed state
Theme flash (admin pages, docs, stats, progress — anywhere SSR'd):
the JWT middlewares built the context user WITHOUT the Theme column,
so every page rendering theme-{user.Theme} from c.Get("user") painted
an empty class (:root tokyo-night default) and only settled on the real
theme after initializeTheme -> loadUserTheme round-tripped the server.
Both middlewares now carry liveUser.Theme into the context user;
verified live: /admin /stats /progress /docs /docs-deep /dashboard
/collections all SSR theme-hackerman.

Defense in depth for the same bug: loadTheme() no longer re-applies
localStorage over a server-rendered body theme (applyTheme records even
the default, so any signed-out visit baked tokyo-night into localStorage
and every signed-in page flipped to it post-paint). Public auth/landing
bodies dropped their hardcoded theme classes so ThemeFlashGuard's
pre-paint html class actually governs them; docs theme <select> now
syncs from the body class and re-syncs after the profile fetch.

Sidebar state (the unpleasant part): the app sidebar's openPanel was
reinitialized from the pathname on every load; it now restores from
localStorage ('sidebar-open-panel') with the pathname default as
fallback and -persists every toggle. Docs sidebar sections/groups
carry data-key attributes and toggleCollapsible persists collapsed
state per key ('docs-nav-state'); restoreDocsNav() applies it at init —
server-rendered defaults still govern first visit.

make test green; rendering ownership OK (no new inline scripts); 146
vitests pass; live-verified SSR classes on 7 pages.
2026-10-24 19:44:34 -04:00

423 lines
18 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import "bookhoard/internal/database"
templ Setup(libraryTypes []database.LibraryTypes) {
<!DOCTYPE html>
<html lang="en">
<head>
@ThemeFlashGuard()
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Setup - Bookhoard</title>
<script src="/static/htmx.min.js"></script>
<script type="module" src="/static/main.js"></script>
<link href="/static/style.css" rel="stylesheet"/>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
</head>
<body class="min-h-screen" x-data="setupWizard" x-init="initSetup()">
<div class="mx-auto container px-4 py-8 max-w-2xl">
<div class="text-center mb-8">
<span class="inline-grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("library", "h-7 w-7")
</span>
<h1 class="text-4xl font-bold mb-2 tracking-tight" style="color: var(--text-primary)">Welcome to Bookhoard</h1>
<p class="text-lg" style="color: var(--text-secondary)">Let's get your library set up.</p>
</div>
<div class="flex items-center justify-center mb-8 gap-2">
<template x-for="n in 4" :key="n">
<div class="flex items-center gap-2">
<div
class="w-10 h-10 rounded-full grid place-items-center font-bold text-sm transition-all"
:class="currentStep >= n ? 'bg-[var(--accent)] text-[var(--bg-primary)]' : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border)]'"
>
<template x-if="currentStep > n">@Icon("check", "h-5 w-5")</template>
<span x-show="currentStep <= n" x-text="n"></span>
</div>
<div
x-text="['Admin', 'Libraries', 'Folders', 'Scan'][n-1]"
class="text-sm hidden sm:inline"
:style="currentStep >= n ? 'color: var(--text-primary)' : 'color: var(--text-secondary)'"
></div>
<div x-show="n < 4" class="w-8 h-0.5 mx-1" :class="currentStep > n ? 'bg-[var(--accent)]' : 'bg-[var(--border)]'"></div>
</div>
</template>
</div>
<div x-show="errorMsg" class="mb-4 card p-4 text-sm" style="border-color: var(--status-danger); background-color: color-mix(in srgb, var(--status-danger) 12%, transparent); color: var(--status-danger);">
<span x-text="errorMsg"></span>
</div>
<!-- ==================== STEP 1: Create Admin ==================== -->
<div x-show="currentStep === 1" class="card p-6">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Administrator Account</h2>
<p class="mb-6" style="color: var(--text-secondary)">This first account will have full admin privileges.</p>
<form @submit.prevent="submitAdmin()">
<div class="mb-6 card p-4" style="background-color: var(--bg-primary);">
<label for="base-url" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL (Base URL)</label>
<input
type="url"
id="base-url"
x-model="admin.baseUrl"
placeholder="https://books.example.com"
class="input mt-2"
required
/>
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
</div>
<div class="mb-4">
<label for="email" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Email</label>
<input
type="email"
id="email"
x-model="admin.email"
class="input mt-2"
required
/>
</div>
<div class="mb-4">
<label for="username" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Username</label>
<input
type="text"
x-model="admin.username"
id="username"
class="input mt-2"
required
/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="first-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">First Name</label>
<input
type="text"
id="first-name"
x-model="admin.firstName"
placeholder="Optional"
class="input mt-2"
/>
</div>
<div>
<label for="last-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Last Name</label>
<input
type="text"
id="last-name"
x-model="admin.lastName"
placeholder="Optional"
class="input mt-2"
/>
</div>
</div>
<div id="password-requirements" class="mb-4 card p-4 text-sm" style="background-color: var(--bg-primary);">
<p class="font-semibold mb-2" style="color: var(--text-primary)">Password Requirements:</p>
<ul class="space-y-1" style="color: var(--text-secondary)">
<li id="req-length" class="flex items-center gap-2">
<span class="requirement-icon">○</span> At least 8 characters
</li>
<li id="req-upper" class="flex items-center gap-2">
<span class="requirement-icon">○</span> One uppercase letter
</li>
<li id="req-lower" class="flex items-center gap-2">
<span class="requirement-icon">○</span> One lowercase letter
</li>
<li id="req-number" class="flex items-center gap-2">
<span class="requirement-icon">○</span> One number
</li>
<li id="req-special" class="flex items-center gap-2">
<span class="requirement-icon">○</span> One special character
</li>
<li id="req-match" class="flex items-center gap-2">
<span class="requirement-icon">○</span> Passwords match
</li>
</ul>
</div>
<div class="mb-4">
<label for="password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Password</label>
<input
type="password"
x-model="admin.password"
id="password"
class="input mt-2"
required
/>
</div>
<div class="mb-6">
<label for="confirm-password" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Confirm Password</label>
<input
type="password"
x-model="admin.confirmPassword"
id="confirm-password"
class="input mt-2"
required
/>
</div>
<button
type="submit"
id="register-btn"
class="btn btn-primary w-full"
:disabled="loading"
x-text="loading ? 'Creating…' : 'Create Admin Account'"
></button>
</form>
</div>
<!-- ==================== STEP 2: Create Libraries ==================== -->
<div x-show="currentStep === 2" class="card p-6">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Create Libraries</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add one or more media libraries. You can always add more later.</p>
<div x-show="libraries.length > 0" class="mb-6 space-y-2">
<template x-for="(lib, idx) in libraries" :key="lib.id">
<div class="card p-3 flex items-center justify-between" style="background-color: var(--bg-primary);">
<div class="flex items-center gap-3">
<span class="grid place-items-center h-9 w-9 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-5 w-5")</span>
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-5 w-5")</span>
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-5 w-5")</span>
</span>
<div>
<p class="font-medium" style="color: var(--text-primary)" x-text="lib.name"></p>
<p class="text-xs capitalize" style="color: var(--text-secondary)" x-text="lib.type"></p>
</div>
</div>
<button
type="button"
@click="removeLibrary(idx)"
class="icon-btn"
style="color: var(--status-danger);"
aria-label="Remove library"
>@Icon("trash", "h-4 w-4")</button>
</div>
</template>
</div>
<form @submit.prevent="addLibrary()" class="space-y-4 card p-4" style="background-color: var(--bg-primary);">
<div>
<label for="lib-name" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Name</label>
<input
type="text"
id="lib-name"
x-model="newLibrary.name"
placeholder="My Ebook Library"
class="input mt-2"
required
/>
</div>
<div>
<label for="lib-type" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Library Type</label>
<select
id="lib-type"
x-model="newLibrary.type"
class="input mt-2"
>
@LibraryTypeOptions(libraryTypes)
</select>
</div>
<div>
<label for="lib-desc" class="block text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Description (optional)</label>
<input
type="text"
id="lib-desc"
x-model="newLibrary.description"
placeholder="Optional"
class="input mt-2"
/>
</div>
<button
type="submit"
class="btn btn-secondary w-full"
:disabled="loading || !newLibrary.name.trim()"
x-text="loading ? 'Creating…' : '+ Add This Library'"
></button>
</form>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 1" class="btn btn-secondary">
@Icon("arrow-left", "h-4 w-4")
Back
</button>
<button type="button" @click="currentStep = 3" class="btn btn-primary">
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip – Continue'"></span>
@Icon("arrow-right", "h-4 w-4")
</button>
</div>
</div>
<!-- ==================== STEP 3: Configure Folders ==================== -->
<div x-show="currentStep === 3" class="card p-6">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Configure Library Folders</h2>
<p class="mb-6" style="color: var(--text-secondary)">Add the filesystem folders where your media files are stored.</p>
<div x-show="libraries.length === 0" class="text-center py-8" style="color: var(--text-secondary)">
<p>No libraries created. You can add folders later from the admin panel.</p>
</div>
<div class="space-y-6">
<template x-for="lib in libraries" :key="lib.id">
<div class="card p-4" style="background-color: var(--bg-primary);">
<div class="flex items-center gap-2 mb-3">
<span class="grid place-items-center h-8 w-8 rounded-lg" style="background-color: var(--accent-muted); color: var(--accent);">
<span class="inline-flex" x-show="lib.type === 'ebooks'">@Icon("book", "h-4 w-4")</span>
<span class="inline-flex" x-show="lib.type === 'comics'">@Icon("book-open", "h-4 w-4")</span>
<span class="inline-flex" x-show="lib.type === 'manga'">@Icon("library", "h-4 w-4")</span>
</span>
<h4 class="font-semibold" style="color: var(--text-primary)" x-text="lib.name"></h4>
</div>
<div class="space-y-1 mb-3" x-show="lib.folders && lib.folders.length > 0">
<template x-for="(folder, fIdx) in (lib.folders || [])" :key="folder">
<div class="flex items-center justify-between p-2 rounded-lg text-sm" style="background-color: var(--bg-secondary);">
<span class="flex items-center gap-2 truncate" style="color: var(--text-primary)">
@Icon("folder", "h-4 w-4 shrink-0")
<span class="truncate" x-text="folder"></span>
</span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="icon-btn shrink-0" style="color: var(--status-danger);" aria-label="Remove folder">@Icon("trash", "h-4 w-4")</button>
</div>
</template>
</div>
<div class="flex gap-2">
<input
type="text"
:placeholder="'/path/to/' + lib.type"
:id="'folderInput-' + lib.id"
class="input flex-1 text-sm"
/>
<button
type="button"
@click="showFolderBrowser('folderInput-' + lib.id)"
class="btn btn-secondary px-3 text-xs"
>Browse</button>
<button
type="button"
@click="addFolderToLibrary(lib)"
class="btn btn-primary px-3 text-xs"
>Add</button>
</div>
</div>
</template>
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 2" class="btn btn-secondary">
@Icon("arrow-left", "h-4 w-4")
Back
</button>
<button type="button" @click="currentStep = 4" class="btn btn-primary">
Next: Scan
@Icon("arrow-right", "h-4 w-4")
</button>
</div>
</div>
<!-- ==================== STEP 4: Scan & Finish ==================== -->
<div x-show="currentStep === 4" class="card p-6">
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary)">Scan & Finish</h2>
<p class="mb-6" style="color: var(--text-secondary)">Review your configuration and start the initial scan.</p>
<div class="space-y-3 mb-6">
<div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Server URL</p>
<p style="color: var(--text-primary)" x-text="admin.baseUrl"></p>
</div>
<div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-xs uppercase tracking-wide font-medium mb-1" style="color: var(--text-secondary)">Administrator</p>
<p style="color: var(--text-primary)" x-text="admin.username + ' (' + admin.email + ')'"></p>
</div>
<div class="card p-4" style="background-color: var(--bg-primary);">
<p class="text-xs uppercase tracking-wide font-medium mb-2" style="color: var(--text-secondary)">
Libraries (<span x-text="libraries.length"></span>)
</p>
<template x-for="lib in libraries" :key="lib.id">
<div class="flex items-start justify-between text-sm py-1">
<div class="flex items-center gap-2">
<span style="color: var(--text-primary)" x-text="lib.name"></span>
<span class="chip" x-text="lib.type"></span>
</div>
<div class="text-right" style="color: var(--text-secondary)">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
</div>
</div>
</template>
</div>
</div>
<div x-show="scanStarted" class="mb-6 space-y-3">
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Progress</h3>
<div id="library-progress-list" class="space-y-3">
<template x-for="job in scanJobs" :key="job.jobId">
<div class="card p-3" style="background-color: var(--bg-primary);">
<div class="flex justify-between items-center mb-2">
<span class="font-medium" style="color: var(--text-primary)" x-text="job.libraryName"></span>
<span class="text-sm" style="color: var(--text-secondary)" x-text="job.statusText"></span>
</div>
<div class="w-full rounded-full h-2" style="background-color: var(--bg-secondary)">
<div
class="h-2 rounded-full transition-all duration-500"
:style="'width: ' + job.progress + '%; background-color: var(--accent);'"
></div>
</div>
</div>
</template>
</div>
<div x-show="scanComplete" class="card p-4 text-sm" style="border-color: var(--status-success); background-color: color-mix(in srgb, var(--status-success) 12%, transparent); color: var(--status-success);">
Initial scan complete! Your libraries are ready to use.
</div>
</div>
<div class="flex justify-between mt-6">
<button type="button" @click="currentStep = 3" class="btn btn-secondary" :disabled="loading">
@Icon("arrow-left", "h-4 w-4")
Back
</button>
<div class="flex gap-3">
<button
x-show="!scanStarted"
type="button"
@click="startScan()"
class="btn btn-primary"
:disabled="loading"
>
@Icon("sync", "h-4 w-4")
Start Scan
</button>
<button
x-show="scanStarted && !scanComplete"
type="button"
@click="finishSetup()"
class="btn btn-secondary"
:disabled="loading"
>Skip to Dashboard</button>
<button
x-show="scanComplete"
type="button"
@click="finishSetup()"
class="btn btn-primary"
:disabled="loading"
x-text="loading ? 'Finishing…' : 'Go to Dashboard'"
></button>
</div>
</div>
</div>
</div>
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
<div class="card p-6 w-full max-w-md mx-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="icon-btn" aria-label="Close">@Icon("close", "h-5 w-5")</button>
</div>
<div id="folder-browser-content">
</div>
</div>
</div>
</body>
</html>
}