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
397 lines
15 KiB
Templ
397 lines
15 KiB
Templ
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>
|
||
}
|