Files
john-okeefe ab465d8e0e feat(ui): adopt book-open brand icon and add SVG favicon
Bring the tighten-ui brand treatment into new-ui:

- Replace the emoji book (📚) in the sidebar header with the book-open
  icon rendered in the theme accent color, matching the tighten-ui
  header brand (templates/header.templ).
- Add web/static/favicon.svg (book-open glyph, tokyo-night accent
  #7aa2f7 stroke) and reference it from the <head> of all 27 page
  templates, so the favicon is present on login/setup/error pages too.
- Regenerate templ output for all affected templates.
2026-08-10 13:43:13 -04:00

422 lines
18 KiB
Templ
Raw Permalink 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"/>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
</head>
<body class="theme-tokyo-night 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"
>
<option value="ebooks">Ebooks</option>
<option value="comics">Comics</option>
<option value="manga">Manga</option>
</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>
}