Files
bookhoard/templates/setup.templ
T
john-okeefe a075a5061f feat(ui): refresh auth, entry, admin, and secondary pages
Apply the new design-system primitives across the remaining pages so the
whole app shares one visual language:

- Auth/entry: redesigned index, login, register (centered card layout,
  SVG icons, semantic tokens)
- Admin: sidebar nav with icons, cohesive admin dashboard/users/library/
  processing-issues/settings (tables use border-line + hover states;
  scan/websocket attributes preserved)
- Secondary: analytics (stat cards), devices, progress, conflicts, queue,
  profile + form/modal all migrated to .card/.btn/.input primitives and
  SVG action icons
- Docs + API explorer + setup wizard + collection/rules/modals +
  unlinked books + book-detail modals: consistent chrome, modal scrims
  use --surface-overlay, close buttons use icon-btn
- reader.templ and error.templ intentionally left unchanged
2026-08-05 16:01:22 -04:00

397 lines
15 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
templ Setup() {
<!DOCTYPE html>
<html lang="en">
<head>
<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"/>
</head>
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()">
<div class="container mx-auto px-4 py-8 max-w-2xl">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold tracking-tight text-content mb-2">Welcome to Bookhoard</h1>
<p class="text-lg text-content-muted">Let's get your library set up.</p>
</div>
<!-- Step indicator -->
<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 flex items-center justify-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)]'"
>
<span x-text="currentStep > n ? '✓' : 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>
<!-- Error display -->
<div x-show="errorMsg" x-cloak class="mb-4 p-4 rounded-lg border border-red-500/50 bg-red-500/10 text-red-400 text-sm">
<span x-text="errorMsg"></span>
</div>
<!-- ==================== STEP 1: Create Admin ==================== -->
<div x-show="currentStep === 1" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Create Administrator Account</h2>
<p class="mb-6 text-content-muted">This first account will have full admin privileges.</p>
<form @submit.prevent="submitAdmin()">
<div class="mb-6 p-4 rounded-lg border border-line bg-surface">
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Server URL (Base URL)</label>
<input
type="url"
x-model="admin.baseUrl"
placeholder="https://books.example.com"
class="input w-full mt-2"
required
/>
<p class="text-sm mt-2 text-content-muted">The public URL of your Bookhoard instance. Used for device sync, OPDS feed, and API endpoints.</p>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Email</label>
<input
type="email"
id="email"
x-model="admin.email"
class="input w-full"
required
/>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Username</label>
<input
type="text"
x-model="admin.username"
id="username"
class="input w-full"
required
/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">First Name</label>
<input
type="text"
x-model="admin.firstName"
placeholder="Optional"
class="input w-full"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Last Name</label>
<input
type="text"
x-model="admin.lastName"
placeholder="Optional"
class="input w-full"
/>
</div>
</div>
<div id="password-requirements" class="mb-4 p-4 rounded-lg border border-line bg-surface text-sm">
<p class="font-semibold mb-2 text-content">Password Requirements:</p>
<ul class="space-y-1 text-content-muted">
<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 class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Password</label>
<input
type="password"
x-model="admin.password"
id="password"
class="input w-full"
required
/>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Confirm Password</label>
<input
type="password"
x-model="admin.confirmPassword"
id="confirm-password"
class="input w-full"
required
/>
</div>
<button
type="submit"
id="register-btn"
class="btn btn-primary w-full opacity-50 cursor-not-allowed"
:disabled="loading"
x-text="loading ? 'Creating...' : 'Create Admin Account'"
></button>
</form>
</div>
<!-- ==================== STEP 2: Create Libraries ==================== -->
<div x-show="currentStep === 2" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Create Libraries</h2>
<p class="mb-6 text-content-muted">Add one or more media libraries. You can always add more later.</p>
<!-- Created libraries list -->
<div x-show="libraries.length > 0" x-cloak class="mb-6 space-y-2">
<template x-for="(lib, idx) in libraries" :key="lib.id">
<div class="p-3 rounded-lg border border-line bg-surface flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<div>
<p class="font-medium text-content" x-text="lib.name"></p>
<p class="text-xs capitalize text-content-muted" x-text="lib.type"></p>
</div>
</div>
<button
type="button"
@click="removeLibrary(idx)"
class="text-xs text-danger hover:underline"
>Remove</button>
</div>
</template>
</div>
<!-- Add library form -->
<form @submit.prevent="addLibrary()" class="space-y-4 p-4 rounded-lg border border-line bg-surface">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Name</label>
<input
type="text"
x-model="newLibrary.name"
placeholder="My Ebook Library"
class="input w-full"
required
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Library Type</label>
<select
x-model="newLibrary.type"
class="input w-full"
>
<option value="ebooks">📚 Ebooks</option>
<option value="comics">📖 Comics</option>
<option value="manga">🗾 Manga</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide text-content-muted mb-1">Description (optional)</label>
<input
type="text"
x-model="newLibrary.description"
placeholder="Optional"
class="input w-full"
/>
</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">Back</button>
<button type="button" @click="currentStep = 3" class="btn btn-primary">
<span x-text="libraries.length > 0 ? 'Next: Configure Folders' : 'Skip Continue'"></span>
</button>
</div>
</div>
<!-- ==================== STEP 3: Configure Folders ==================== -->
<div x-show="currentStep === 3" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Configure Library Folders</h2>
<p class="mb-6 text-content-muted">Add the filesystem folders where your media files are stored.</p>
<div x-show="libraries.length === 0" x-cloak class="text-center py-8 text-content-muted">
<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="p-4 rounded-lg border border-line bg-surface">
<div class="flex items-center gap-2 mb-3">
<span class="text-xl" x-text="lib.type === 'ebooks' ? '📚' : lib.type === 'comics' ? '📖' : '🗾'"></span>
<h4 class="font-semibold text-content" x-text="lib.name"></h4>
</div>
<!-- Existing folders for this library -->
<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 text-sm border border-line bg-surface-raised">
<span class="text-content" x-text="folder"></span>
<button type="button" @click="removeFolder(lib.id, folder, fIdx)" class="text-xs text-danger hover:underline">Remove</button>
</div>
</template>
</div>
<!-- Add folder -->
<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 text-xs"
>Browse</button>
<button
type="button"
@click="addFolderToLibrary(lib)"
class="btn btn-primary 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">Back</button>
<button type="button" @click="currentStep = 4" class="btn btn-primary">Next: Scan</button>
</div>
</div>
<!-- ==================== STEP 4: Scan & Finish ==================== -->
<div x-show="currentStep === 4" x-cloak class="card p-6">
<h2 class="text-2xl font-bold tracking-tight text-content mb-2">Scan & Finish</h2>
<p class="mb-6 text-content-muted">Review your configuration and start the initial scan.</p>
<!-- Summary -->
<div class="space-y-3 mb-6">
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1 text-content-muted">Server URL</p>
<p class="text-content" x-text="admin.baseUrl"></p>
</div>
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-1 text-content-muted">Administrator</p>
<p class="text-content" x-text="admin.username + ' (' + admin.email + ')'"></p>
</div>
<div class="p-4 rounded-lg border border-line bg-surface">
<p class="text-sm font-medium mb-2 text-content-muted">
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>
<span class="text-content" x-text="lib.name"></span>
<span class="ml-2 text-xs px-2 py-0.5 rounded bg-brand text-surface" x-text="lib.type"></span>
</div>
<div class="text-right text-content-muted">
<span x-text="(lib.folders || []).length + ' folder(s)'"></span>
</div>
</div>
</template>
</div>
</div>
<!-- Scan progress -->
<div x-show="scanStarted" x-cloak class="mb-6 space-y-3">
<h3 class="font-semibold text-content">Scan Progress</h3>
<div id="library-progress-list" class="space-y-3">
<template x-for="job in scanJobs" :key="job.jobId">
<div class="p-3 rounded border border-line bg-surface">
<div class="flex justify-between items-center mb-2">
<span class="font-medium text-content" x-text="job.libraryName"></span>
<span class="text-sm text-content-muted" x-text="job.statusText"></span>
</div>
<div class="w-full rounded-full h-2 bg-surface-raised">
<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" x-cloak class="p-4 rounded-lg border border-green-500/50 bg-green-500/10 text-green-400 text-sm">
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">Back</button>
<div class="flex gap-3">
<button
x-show="!scanStarted"
x-cloak
type="button"
@click="startScan()"
class="btn btn-primary"
:disabled="loading"
>Start Scan</button>
<button
x-show="scanStarted && !scanComplete"
x-cloak
type="button"
@click="finishSetup()"
class="btn btn-secondary"
:disabled="loading"
>Skip to Dashboard</button>
<button
x-show="scanComplete"
x-cloak
type="button"
@click="finishSetup()"
class="btn btn-primary"
:disabled="loading"
x-text="loading ? 'Finishing...' : 'Go to Dashboard'"
></button>
</div>
</div>
</div>
</div>
<!-- Folder Browser Modal (reused from admin library) -->
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" 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 text-content">Browse Folders</h2>
<button type="button" @click="hideFolderBrowser()" class="icon-btn">
@Icon("close", "h-5 w-5")
</button>
</div>
<div id="folder-browser-content">
<!-- Directory listings rendered by Alpine -->
</div>
</div>
</div>
</body>
</html>
}